# Project Details (Draft)

## Overview
This document is a structured, living description of the data layout and processing pipeline for the current active parts of the `imagej-elphel` project. It consolidates earlier notes and the latest additions into a coherent reference.

## Data Layout and List Files
The program starts with a list file that defines the root and source directories plus scene sequences. Example list entry:

```
1763232231_345331   1, -2, -1, 0, 0  # NC32-LY 13:43:51 #nc34a
```

The first token is a scene-sequence timestamp (directory name). The next two numbers define the scene index range to process. For example, with 499 subfolders (0..498), `1, -2` means process scenes 1..497.

Each subdirectory in the sequence is a "scene" (a set of 16 simultaneously captured LWIR images). Sensors are arranged on a 220 mm diameter circle, evenly spaced at 360/16 degrees. Sensor 0 is at the top; 1..15 are clockwise when viewed from behind the camera (same direction as the camera looks). Sensors 16..19 are RGB cameras and are not used in the current pipeline.

The raw LWIR data under:

```
/media/elphel/btrfs-data/lwir16-proc/NC/scenes/
```

is copied once into model directories (as `jp4` subfolders). Example:

```
/media/elphel/btrfs-data/lwir16-proc/NC/scenes/1763232231_345331/1763232231_362003
```

is copied to:

```
/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145/1763232231_362003/jp4
```

IMU data for the scene is stored as:

```
/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145/1763232231_362003/1763232231_362003-ims.corr-xml
```

The IMU source path currently comes from `CORRECTION_PARAMETERS.getImsSourceDirectory()` (edited in "Setup CLT Batch parameters") and is not yet exposed in `*.list` files.

Scene sequences are captured at ~60 Hz for ~8 seconds, followed by ~3 seconds pause due to bandwidth constraints.

## Model Directories, Versions, and Linked Views
The parent directory for processed scenes is large (the latest set contains ~85K model directories). Most contain only `jp4` and IMS data. Some contain additional results and are considered special:

- Reference scenes with computed data for their segment.
- Index scenes (latest timestamp per sequence) that include `*-ground_planes.csv`. The first column contains timestamps of reference scenes in the segment.

Processing results are versioned. The version directory is defined by `EyesisCorrectionParameters.x3dModelVersion` (configured in “Setup CLT Batch parameters”). A single model directory may contain multiple versions.

For manual browsing, the program creates a directory with symlinks to special model directories. Example:

```
/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145
```

## Core Processing Pipeline (Segment-Based)
Scene sequences are processed separately in a loop such as:

```
for (int nseq = 0; nseq < num_seq; nseq++) {
    long start_time_seq = System.nanoTime();
    System.out.println("\nSTARTED PROCESSING SCENE SEQUENCE "+nseq+" (last is "+(num_seq-1)+")\n");
    ...
}
```

Segments are defined around a reference scene, with overlap and matching constraints. Scenes are processed in reverse order (latest first) for historical reasons.

### Segment Bounds
The program finds how many preceding scenes can be included in the segment, based on overlap and the number of good matching tiles. Scenes can be "partially valid" if overlap is sufficient for translation but not for zoom or rotation fitting.

In the current application (Cessna, 25–150 m AGL, 30–40 m/s), the reference scene is near the center of the segment. Earliest and latest scenes may not overlap with each other but must overlap with the reference. The segment is continuous with no gaps. The earliest scene is determined by overlap with the reference scene (`EstimateSceneRange.getPrescanRange()`).

### Initial Depth Map (Intrascene)
Depth is computed per tile (80x64 tiles corresponding to 640x512 images) in `OpticalFlow.buildRefDSI()`. This uses intrascene correlation (parallax across sensors) and GPU/CUDA kernels under:

```
/home/elphel/git/imagej-elphel/src/main/resources/kernels
```

Java code uses JCuda (`/home/elphel/git/imagej-elphel/src/main/java/com/elphel/imagej/gpu`). Kernels are debugged via Nvidia NSight using a C++ testbench. The external CUDA repo is:

```
/home/elphel/git/tile_processor_gpu/
https://git.elphel.com/Elphel/tile_processor_gpu/tree/lwir16_2
```

Because baselines are narrow, keypoints are not used. Instead, the pipeline uses 2D phase correlation between 16x16 tiles. There are 120 sensor pairs. Pairwise results are combined by:

- Baseline-rotated/scaled correlations.
- LMA fit to all 120 pairs (more accurate, more expensive).
- DNN approach (from the arXiv paper, not yet implemented for the current camera).

Argmax accuracy is improved by an iterative approach. After each disparity estimate, a fractional shift is applied in the frequency domain and correlation is repeated. Residual disparity converges to zero without assuming correlation shape. The phase-correlation step uses a "fat zero" (small constant in the denominator) to balance noise sensitivity.

The initial disparity map is saved as:

```
.../v89/<timestamp>-DSI_MAIN.tiff
```

### Pose Estimation (Interscene)
The pipeline estimates per-scene pose relative to the reference scene. Positions are stored in `*-egomotion.csv` columns 3–5 and orientation in columns 6–8. IMS provides hints. Matching is done with interscene correlation on per-tile motion vectors.

Workflow:
- Build initial disparity map.
- Match consecutive scenes to estimate poses.
- Refine disparity map with interscene accumulation.
- Repeat with more LMA parameters enabled.

Outputs include 3D scene models, DNN training data, and auxiliary files. State is persisted in:

- `*-DSI_MAIN.tiff`
- `*-INTER-INTRA-LMA.tiff` (refined disparity, overwritten per iteration)
- `*-INTERFRAME.corr-xml` (poses, overwritten per iteration)

Iteration counters:

- `EYESIS_DCT_AUX.num_orient`
- `EYESIS_DCT_AUX.num_accum`

Used in loops like:

```
while ((master_CLT.getNumOrient() < min_num_orient) || (master_CLT.getNumAccum() < min_num_interscene))
```

### ERS and Motion Blur
Electronic Rolling Shutter (ERS) is handled during pose fitting. Each step computes per-tile motion vectors, then uses LMA to fit pose and optional velocities. Since motion blur can exceed 5 px, images are pre-aligned using IMS predictions. When IMS is unavailable, a spiral search is used. Matching starts from scenes closest to the reference, then expands outward.

#### GPU-side motion-blur handling and TD correlation flow
Current interscene matching for a segment uses a GPU-centric transform-domain (TD) pipeline:

1) The reference scene (all 16 LWIR images) is transferred to GPU memory and converted to TD once.
2) Factory-calibration aberration correction is applied in TD by convolution with the calibration kernels.
3) Motion-blur compensation is applied in TD workflow for both reference and matched scenes:
   - microbolometer blur is modeled as a first-order LPF in time;
   - correction subtracts a scaled copy of the same image shifted by motion vectors (tile-granular approximation);
   - task definitions are packed in Java (`TpTask`) and passed as float task arrays to GPU kernels.

For each matched scene, the 16 images are TD-transformed and geometrically aligned to the reference (tile by tile), using integer tile placement plus fractional subpixel shift through TD phase rotation. Pairwise products with cached reference TD are computed per sensor pair, then all 16 channels are accumulated in TD before phase-only normalization. A small "fat zero" term in normalization controls SNR vs peak sharpness tradeoff. After inverse transform, the peak argmax gives tile motion vectors (`mvX`, `mvY`), and peak magnitude is used as sample strength/weight for subsequent LMA.

This "accumulate in TD before normalization" step is important for low-contrast LWIR data because it improves robustness relative to per-channel independent normalization.

### FPN Mitigation
Uncooled LWIR sensors have fixed-pattern noise (FPN). If interscene pixel shifts are too small, correlation may lock to FPN. Scenes with expected shifts below thresholds (`IntersceneMatchParameters.fpn_min_offset`, `min_offset`) are skipped and added to `fpn_list`. Later, each skipped scene is matched to a farther scene using `Interscene.getFPNPairs`, ensuring pixel shifts exceed tile size and avoid FPN dominance.

## Differential Image Rectification (TP/CLT)
TPNET uses "differential image rectification" (US10638109B2). Instead of full rectification to a pinhole model, the pipeline computes a virtual average camera and rectifies only the small differential distortions of each sensor relative to that average. Local shifts are typically <2 pixels.

CLT (Complex Lapped Transform) is used instead of FFT. CLT has perfect reconstruction and allows lossless fractional pixel shifts via phase rotations. This enables sub-pixel disparities (~0.05–0.01 px) without upsampling or resampling. Multiple operations (aberration correction and phase correlation) are combined in the transform domain for GPU efficiency.

## Down-View Aerial Applications (SfM / SLAM)
Downward-looking aerial views reduce depth variation, which makes separating translation from rotation harder. Lateral motion and azimuth rotation both appear as X shifts; vertical motion and tilt appear as Y shifts. Traditional intrascene disparity becomes insufficient for high altitudes, so the workflow was adjusted.

### Multicopter (Mine Detection) workflow
1) Initial intrascene depth map
2) Initial segment pose estimation
3) Depth refinement via interscene accumulation
4) Pose refinement with more LMA parameters
5) SfM refinement, repeated multiple times
6) Calibration (IMS vs camera frame)
7) Outputs

### Foliage Penetration: SLAM approach
At high altitudes, pure intrascene depth fails for long baselines. The pipeline uses SfM simultaneously with pose estimation in `EstimateSceneRange.scanSfmIMS()`. Disparity is refined as scenes get farther from the reference, with weights proportional to baseline distance. Short baselines skip SfM (`min_sfm_meters`). Long baselines may filter low-confidence tiles (`sfma_filt_meters`). Background tiles are preferred when FG/BG confusion occurs.

### Current Limitations
- Disparity offset compensation is still incomplete. In navigation, far objects allowed treating minimal disparity as infinity; for down-view this is not possible. Current correction attempts use:
  - `OpticalFlow.getImgImsScale()`
  - `OpticalFlow.getImsDisparityCorrection()`
  - `QuadCLT.offsetDSI()`
  - `QuadCLT.setDispInfinityRef()`
  - `OpticalFlow.scaleImgXYZ()`

This fails in some low-altitude forest cases. The correction should be per-sequence, not per-segment, for better accuracy.

- IMS/camera orientation offset (`Interscene.getQuaternionCorrection()`) is disabled and should move to per-sequence. `QuaternionLma` should be restored to use linear motion plus rotations. We should weight angular samples (closer to reference are more accurate) and ensure time consistency. Angular data is in `*-egomotion.csv` columns 6–8.

## CUAS vs Moving-Camera Modes (Operational Difference Notes)
These notes describe the practical difference between:
- the moving-camera aerial pipeline (foliage/restoration), and
- the CUAS pipeline with gimbal rotation for target detection.

### Moving-camera aerial mode (current foliage/global-LMA development)
- Camera platform is moving.
- A scene sequence (typically ~500 scenes) is split into overlapping segments.
- Each segment is built around a captured center/reference scene.
- Scenes are transformed to be viewed from that segment reference pose.
- Matching/optimization is segment-based because far scenes may not overlap directly.

### CUAS mode (gimbal-rotation, mostly parked after 2025-10-27)
- Camera is fixed in ground frame while gimbal rotates view direction around a fixed axis.
- A virtual reference scene is used (`center_CLT = QuadCLT.restoreCenterClt(...)`) rather than a captured reference frame.
- Typical scan: about one full revolution per ~3 seconds (~0.33 Hz), with rotation radius around ~3 angular degrees.
- Resulting inter-frame shift remains small (roughly <10% of image size), so all scenes in a long sequence (~500) can overlap a single virtual center.
- Full-sequence processing is therefore feasible without segment splitting logic used in moving-camera mode.

### Why CUAS uses rotation
The main target class is difficult: simultaneously low-contrast, small, and slow apparent motion in pixels.  
FPN (fixed-pattern noise) in microbolometer LWIR sensors can dominate this regime.

Rotation introduces faster target/background variation in image coordinates while FPN drifts more slowly, improving separability.

### FPN behavior and mitigation in CUAS
- FPN is not perfectly static over long periods.
- Boson-class sensors use periodic shutter-based correction (multiple dark frames every configured interval), which reduces but does not eliminate FPN.
- CUAS background extraction uses rotation-compensated averaging and subtraction of the averaged background.
- Two FPN components are targeted:
  - persistent sensor/lens-embedded patterns (defects, dirt, etc.),
  - row/column-correlated noise (sensor-architecture correlated stripes).
- Additional row/column normalization (1D row/column averages) is used before/with subtraction.

### Code-state context
- CUAS-focused work was parked around commit `8946631f767ddda77365ebc32d45eef1e3d21936` (October 27, 2025).
- Since then, development emphasis shifted to foliage/global-LMA paths; CUAS branches may require restoration and re-validation for bit-rot before new-data testing.

### Current CUAS target-detection flow (March 2026 LV data)
For the March 2026 LV runs, the current working input list is:

```
/home/elphel/lwir16-proc/LV/lists/lv_site_05.list
```

Per-sequence outputs are created under linked center directories such as:

```
/home/elphel/lwir16-proc/LV/linked/centeres_1773389747-1773390382/1773389818_152542-CENTER/v07
```

The first restored CUAS stage currently produces merged LWIR stacks after FPN and row/column mitigation. A typical file is:

```
1773389818_152542-CENTER-CUAS-MERGED-CUAS.tiff
```

In this stack:
- the first slice is the sequence average,
- the remaining slices are individual scenes with that average subtracted.

The corresponding unsharp-masked version, for example:

```
1773389818_152542-CENTER-CUAS-MERGED-CUAS-UM2.0_1.000_250.tiff
```

is the same merged sequence after unsharp masking (for the example above: sigma 2.0 px, amount 1.0).

The next CUAS stage is the moving-target stage:
- estimate a prevailing per-tile motion vector using 2D phase correlation on the merged/unsharp sequence,
- use those vectors as a "virtual moving camera",
- shift each contributing frame according to that motion vector,
- accumulate the shifted frames as a long exposure to improve SNR of dim moving targets,
- then locate/freeze targets on the accumulated data and render target overlays/video products.

This stage is where the sky mask matters operationally: terrain-rich areas are suppressed before accepting local maxima, so target search is effectively constrained to the allowed sky region.

#### CUAS motion-scan details: keyframes, pair geometry, and FAST/SLOW split
The per-tile motion vectors are not estimated from a single frame pair. For each keyframe index, the code builds a short temporal block and accumulates multiple pairwise correlations that all correspond to nearly the same constant-velocity target motion.

The main scan geometry is controlled by:

- `cuas_corr_offset`: separation between the two halves of each pair set
- `cuas_corr_pairs`: number of correlation pairs accumulated inside one keyframe block
- `cuas_half_step`: if `true`, adjacent keyframes advance by `cuas_corr_offset/2`; otherwise by `cuas_corr_offset`
- `cuas_precorr_ra` and `cuas_corr_step`: optional temporal smoothing/decimation before correlation

For one keyframe:

- `seq_length = cuas_corr_offset + cuas_corr_pairs`
- `corr_inc = cuas_half_step ? (cuas_corr_offset / 2) : cuas_corr_offset`
- for keyframe `n`, `frame0 = start_frame + n * corr_inc`
- `frame1 = frame0 + cuas_corr_offset`

Inside that keyframe, the code correlates multiple temporally aligned pairs:

- `(frame0 + dframe)` against `(frame1 + dframe)`
- where `dframe` runs from `corr_ra_step/2` to `< cuas_corr_pairs`, stepping by `corr_ra_step`

So with the common mental model of `offset = 8` and `pairs = 8`, the effective pairs are approximately:

- `0 vs 8`
- `1 vs 9`
- ...
- `7 vs 15`

or the decimated equivalent if `cuas_corr_step > 1`.

Each pair is correlated tile-by-tile in transform domain (`TDCorrTile`). The pair results are accumulated before conversion back to pixel-domain correlation. If `cuas_smooth` is enabled, the individual pair contributions are weighted by a sine window across the temporal block. After accumulation:

- TD correlations are normalized and converted to pixel-domain using `cuas_fat_zero`
- per-tile local maxima are extracted
- centroid recentering (`cuas_cent_radius`, `cuas_n_recenter`) estimates `vx`, `vy`, peak strength, and in-center fraction

The `*-CORR2D.tiff` files visualize these per-tile 15x15 correlation maps, one keyframe per slice.

FAST and SLOW currently share this same motion-scan geometry. The difference is only in the temporal prefiltering of the input sequence:

- FAST: `temporalUnsharpMask()` using `cuas_temporal_um`
- SLOW: `runningGaussian()` using `cuas_slow_ra`

Both then call the same `prepareMotionBasedSequence()` path and therefore use the same `cuas_corr_offset`, `cuas_corr_pairs`, `cuas_half_step`, `cuas_precorr_ra`, and `cuas_corr_step`.

Practical consequence: if targets are too fast and the peaks in `*-FAST-CORR2D.tiff` hit the `+-7` tile borders, reducing `cuas_corr_offset` will reduce the apparent motion in both FAST and SLOW modes. There is currently no separate fast-only correlation offset parameter; adding one would require a code change.

Velocity units in the current implementation are intentionally split:

- `RSLT_VX` / `RSLT_VY` store the raw displacement measured by the correlation peak over one pair separation, i.e. over `cuas_corr_offset` scenes.
- To convert these stored values to pixels per frame, multiply by `1.0 / cuas_corr_offset`.
- To convert them to displacement between neighboring keyframe centers, multiply by `getCorrIncExact() / cuas_corr_offset`.

Odd `cuas_corr_offset` with `cuas_half_step=true` needs special care:

- the scan index increment is still integer (`getCorrInc()` truncates `cuas_corr_offset / 2`),
- but the intended keyframe-center geometry remains half the full offset (`getCorrIncExact()` uses `0.5 * cuas_corr_offset`),
- so midpoint / before-after chaining logic must use the exact geometric half-step, not the truncated scan increment.

This distinction is what caused the temporary `1.25` compensation when `cuas_corr_offset=5`: the scan step became `2`, but the intended half-step geometry was still `2.5`.

### Where the current code does this
- `CuasRanging.processMovingTargetsMulti()` renders the per-sequence `...-CUAS...` stack and applies the unsharp mask before creating `fpixels`.
- `CuasMotion.processMovingTargetsMulti()` runs the fast/slow motion-target preparation and resolves non-conflicting motion candidates.
- `CuasMotion.generateExtractFilterMovingTargets()` expands the target field, performs the motion-compensated accumulation, and renders target/background outputs.
- `CuasMotion.shiftAndRenderAccumulate()` is the "virtual moving camera / long exposure" step.
- `CuasMotion.getAccumulatedCoordinates()` searches the accumulated tiles for isolated local maxima, applies the sky mask, and refines target coordinates with the CUAS LMA fit.

### Top-level trigger path
The March 2026 CUAS workflow order is:

1. Normal per-sequence CUAS scene processing (`cuas_proc_mode = 0`) produces the merged/unsharp stacks, motion vectors, accumulated target frames, and per-sequence CUAS outputs.
2. `CUAS Combine` runs later on the already generated linked centers (`CuasMultiSeries.processGlobals()`).
3. `CUAS Video` runs after that to combine/package already produced CUAS video outputs.

So the motion-vector and long-exposure stage belongs to the first, per-sequence CUAS processing pass, while `CUAS Combine` and `CUAS Video` are later-stage steps.

The direct gate for producing the accumulated target outputs is:

```
IntersceneMatchParameters.cuas_generate
```

shown in the UI as:

```
Generate and save detected targets
```

If that checkbox is off, the code will still do earlier target/ranging work but will skip the accumulated target TIFF/video generation stage.

### Operational note: sequence continuity is required for CUAS target detection
There is now an explicit distinction between:

- the early per-sequence preparation stages (initial orientation, center/reference generation, FPN extraction), and
- the later target-detection stages that depend on temporal continuity.

The local switch:

```
clt_parameters.imp.cuas_targets_en
```

is used to disable target detection while still allowing the earlier orientation/FPN stages to run. This is useful when debugging sequence geometry, IMS consistency, or FPN extraction without generating target products.

For CUAS processing, a real camera movement or a sharp IMU orientation discontinuity inside one source-list sequence is considered invalid input, not a condition to be automatically "recovered". The later CUAS stages use running temporal references, including reuse of the previous `~8 s` sequence when building the reference image for target detection. If the camera moved in between, that reference image no longer matches the later data.

Operational rule:

- if the camera was moved, or the IMS quaternion has a discontinuity large enough to break overlap/continuity, split the sequence;
- do not reuse the previous running-average reference across that split;
- process the "good" part of the sequence separately from the later part;
- if needed, use `.list` `first/last` trimming to isolate the valid sub-range;
- for the later sub-range, start a new sequence rather than continuing through the bad transition.

As of April 2026, the CUAS code is intentionally conservative here: if initial orientations fail in CUAS mode because IMS/orientation input is inconsistent, processing aborts with a log message instead of retrying the same bad tail indefinitely.

### Next calibration step: target-assisted LY for LV data
The next planned work after restoring/stabilizing CUAS detection is to improve LWIR field calibration ("Lazy Eye", `LY`) for the Latvia (`LV`) data.

Calibration is currently understood as two layers:

1. Factory calibration:
   - uses the special `3.0 m x 7.0 m` curved checkerboard target,
   - measures lens distortion / aberration, including non-radial terms,
   - is assumed fixed for the current camera unless the optics themselves change.
2. Field calibration / `LY`:
   - keeps the lens model fixed,
   - adjusts residual per-sensor attitude errors only,
   - is split into:
     - convergence/divergence (`disparity at infinity`),
     - all remaining relative attitude corrections.

The existing `LY` implementation already supports multiscene adjustment. The planned new functionality is not a new solver; it is a new way to prepare the calibration data for that existing multiscene `LY` code.

Why this is needed for LV:

- the 2025 data often had textured clouds across much of the sky, so ordinary scene content was sufficient for `LY`,
- the Latvia data has mostly clear blue sky,
- usable static texture is concentrated near the bottom of the FOV,
- current calibration is good enough for target detection,
- but accurate ranging needs much better residual disparity accuracy.

Planned approach:

- first restore and validate moving-target detection/tracking,
- then use detected targets themselves as additional `LY` features,
- combine multiple scenes so the selected calibration data covers most of the FOV, especially the corners,
- reuse the simultaneity of each LWIR scene: each accepted target contributes measurements from all 16 sensors at the same timestamp.

One likely implementation path is to build a composite/synthetic calibration scene:

- instead of a few targets (`~5`) per timestamp, gather targets from many timestamps,
- keep each accepted target as a simultaneous 16-view measurement from one scene,
- merge many such measurements into a synthetic scene or multiscene bundle,
- effectively present the `LY` stage with a much denser set of valid points (`~500` targets instead of `~5`), while still respecting the per-scene simultaneity needed by the geometry.

Operational goal:

- use textured terrain where available,
- augment it with detected airborne targets (mostly birds in the current LV data, possibly drones later),
- solve `LY` on a selected subset of scenes that together span the full FOV,
- then reuse the improved `LY` correction for final disparity/ranging runs.

This work should stay separate from factory calibration logic. The intended change is in scene/feature preparation and selection, not in the underlying multiscene `LY` optimizer.

## Latest Additions
### Segment freezing with `keep_segments`
Index scenes (`*-index`) contain `*-INTERFRAME.corr-xml` with keys like:

```
<entry key="EYESIS_DCT_AUX.refscenes_<timestamp>"></entry>
```

The new parameter:

```
IntersceneMatchParameters.keep_segments = true
```

If true, and refscenes exist in the index scene’s `*-INTERFRAME.corr-xml`, those reference scenes are reused instead of recomputing segment splits. This preserves segment definitions when re-running with new parameters or calibrations.

### Elevation histograms for quick classification
Files like:

```
.../v88/<timestamp>-elevations_histogram.csv
```

contain cumulative histograms of tile elevations relative to a fitted ground plane. Columns:

- `AGL`: single value in row 2, altitude above ground.
- `ELEVATION`: elevation value for histogram bin.
- `FRACTION_LOWER`: fraction of tiles below that elevation.

Parameters in `IntersceneMatchParameters`:

```
fgnd_hist_gen = true
fgnd_hist_suffix = "-elevations_histogram.csv"
fgnd_hist_frac = 0.03
fgnd_hist_bins = 100
```

`fgnd_hist_frac` defines outlier trimming; min/max elevation are computed from the remaining tiles. These histograms enable fast, low-bandwidth MCP classification by reading small CSVs instead of large images. Example classification features:

- AGL per segment
- "Forestation ratio" (fraction of tiles above a height threshold, e.g. 1 m)
- Maximum elevation at a given FRACTION_LOWER (e.g. 0.9)

Suggested aggregation: for each scene sequence, compute average/min/max AGL and forestation ratio across reference scenes.

### Global LMA objective and correlation modes (implementation notes)
The global LMA stage is intended to solve one joint optimization problem, not separate ones:

- Correlation residuals for all selected scene/reference pairs (currently scene-to-center, later also 1/4 and 3/4 references).
- Inter-scene LPF/curvature penalties.
- Optional pull-to-target penalties.

The practical requirement is to keep these terms explicit and inspectable in one place in the code:

- `prepareLMA()` should build one combined `y_vector` and one combined `w_vector`.
- Correlation rows should be concatenated first, then LPF rows, then pull rows.
- `getFxDerivs()` should contain visible blocks for each term group in that same order.

#### Correlation residual representation
The single-scene pair LMA supports two measurement modes:

1. Direct XY mode:
   - Two residual components per tile are used as-is (`dx`, `dy`).
2. Eigenvector mode (preferred in current data):
   - Correlation peak shape (ellipse) is used to weight directional certainty.
   - Residuals and derivatives are transformed into eigen directions.
   - Direction perpendicular to linear texture carries most information; along-line direction is de-emphasized.

Current development/testing mostly uses mode 2 (eigenvector mode), but mode 1 should remain supported and periodically checked to avoid bit-rot.

#### Why this structure matters
Keeping all residual segments in one explicit Jacobian path makes it easier to:

- Verify convergence term-by-term.
- Diagnose whether LPF/pull are dominating correlation, or vice versa.
- Add new residual segments incrementally without losing traceability of `y - f(x)` composition.

## Parked State Snapshot (2026-02-25)
This section captures the latest validated state before pausing Global LMA work for C-UAS field-test preparation.

### What is validated now
- Quarter references are selected and used in Global LMA flow.
- Quarter `-INTER-INTRA-LMA.tiff` prerequisites are generated before Global LMA when missing.
- Force regeneration path is working (`force quarter regeneration = true`).
- SfM-only quarter generation path is running and bypassing pose/LMA adjustment in the quarter build stage.
- Regenerated quarter files looked reasonable on manual visual inspection.
- Full run continues into Global LMA and finishes normally.

### Last known-good config for quarter-force + sfm-only
- `/media/elphel/btrfs-data/lwir16-proc/NC/config/MI250-v89-nc_site_37A-FORCE-SFM-XYAT-lpf_XYZ100-ATR3-freeze-RMS1e-4-ratio100-quat4-tw0.0005-cw1.0-noperseries-regularization0.8-transl-rot-nocombo-no_out-preseries_only.corr-xml`

### Key files from the validated run
- Quarter 1:
  `/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145/1763233239_481130/v89/1763233239_481130-INTER-INTRA-LMA.tiff`
- Quarter 3:
  `/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145/1763233241_531813/v89/1763233241_531813-INTER-INTRA-LMA.tiff`
- Baseline copy for comparison:
  `/media/elphel/btrfs-data/lwir16-proc/NC/models/1763232117-1763234145/1763233240_531480/debug01/46`

### Current run pattern (stable for automation)
1. Restore config.
2. Optional parameter changes in `Setup CLT` (`Inter-Global-LMA` section).
3. Save config revision.
4. Run `Aux Build Series`.
5. Inspect console + CSV/TIFF outputs; archive debug artifacts as needed.

### Next TODO (priority order)
1. Performance pass: identify current bottlenecks and low-hanging optimizations.
2. Batch replay of this quarter+global stage on previously processed data; classify failures and choose representative/challenging short test sequences.
3. Algorithm improvement for occlusion handling:
   - Predict likely-occluded tiles from depth/disparity behavior.
   - Conditionally zero tile weights even when correlation strength is high, if non-occluded data is sufficient.
   - Keep enough constraints for small-overlap scenes where joint XYZATR fitting can become underconstrained.
4. Add global-LMA-style disparity/depth refinement stage:
   - Start from center + quarter maps.
   - Reuse pairwise correlations and include occlusion-aware weighting.
   - Revisit FG/BG disparity slices in `-INTER-INTRA-LMA.tiff` (currently mostly pass-through/copy).
5. Deeper threading/performance tuning after correctness is stable.
