sarpyx 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docs/examples/advanced/batch_processing.py +1 -1
- docs/examples/advanced/custom_processing_chains.py +1 -1
- docs/examples/advanced/performance_optimization.py +1 -1
- docs/examples/basic/snap_integration.py +1 -1
- docs/examples/intermediate/quality_assessment.py +1 -1
- outputs/baseline/20260205-234828/__init__.py +33 -0
- outputs/baseline/20260205-234828/main.py +493 -0
- outputs/final/20260205-234851/__init__.py +33 -0
- outputs/final/20260205-234851/main.py +493 -0
- sarpyx/__init__.py +2 -2
- sarpyx/algorithms/__init__.py +2 -2
- sarpyx/cli/__init__.py +1 -1
- sarpyx/cli/focus.py +3 -5
- sarpyx/cli/main.py +106 -7
- sarpyx/cli/shipdet.py +1 -1
- sarpyx/cli/worldsar.py +549 -0
- sarpyx/processor/__init__.py +1 -1
- sarpyx/processor/core/decode.py +43 -8
- sarpyx/processor/core/focus.py +104 -57
- sarpyx/science/__init__.py +1 -1
- sarpyx/sla/__init__.py +8 -0
- sarpyx/sla/metrics.py +101 -0
- sarpyx/{snap → snapflow}/__init__.py +1 -1
- sarpyx/snapflow/engine.py +6165 -0
- sarpyx/{snap → snapflow}/op.py +0 -1
- sarpyx/utils/__init__.py +1 -1
- sarpyx/utils/geos.py +652 -0
- sarpyx/utils/grid.py +285 -0
- sarpyx/utils/io.py +77 -9
- sarpyx/utils/meta.py +55 -0
- sarpyx/utils/nisar_utils.py +652 -0
- sarpyx/utils/rfigen.py +108 -0
- sarpyx/utils/wkt_utils.py +109 -0
- sarpyx/utils/zarr_utils.py +55 -37
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/METADATA +9 -5
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/RECORD +41 -32
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/WHEEL +1 -1
- sarpyx-0.1.6.dist-info/licenses/LICENSE +201 -0
- sarpyx-0.1.6.dist-info/top_level.txt +4 -0
- tests/test_zarr_compat.py +35 -0
- sarpyx/processor/core/decode_v0.py +0 -0
- sarpyx/processor/core/decode_v1.py +0 -849
- sarpyx/processor/core/focus_old.py +0 -1550
- sarpyx/processor/core/focus_v1.py +0 -1566
- sarpyx/processor/core/focus_v2.py +0 -1625
- sarpyx/snap/engine.py +0 -633
- sarpyx-0.1.5.dist-info/top_level.txt +0 -2
- {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/entry_points.txt +0 -0
sarpyx/processor/core/focus.py
CHANGED
|
@@ -35,7 +35,7 @@ from ..utils.viz import dump
|
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
# ---------- Global settings ----------
|
|
38
|
-
environ['OMP_NUM_THREADS'] = '
|
|
38
|
+
environ['OMP_NUM_THREADS'] = '16' # Set OpenMP threads for parallel processing
|
|
39
39
|
__VTIMING__ = False
|
|
40
40
|
|
|
41
41
|
|
|
@@ -529,20 +529,17 @@ class CoarseRDA:
|
|
|
529
529
|
else:
|
|
530
530
|
raise ValueError(f'Backend {self._backend} not supported')
|
|
531
531
|
|
|
532
|
-
#
|
|
533
|
-
expected_shape = (self.len_az_line, self.len_range_line)
|
|
534
|
-
if self.radar_data.shape != expected_shape:
|
|
535
|
-
raise RuntimeError(f'FFT changed radar data shape from {expected_shape} to {self.radar_data.shape}')
|
|
536
|
-
|
|
532
|
+
# Note: Range dimension is padded for linear convolution, so shape will differ from original
|
|
537
533
|
if self._verbose:
|
|
538
534
|
print(f'FFT output data shape: {self.radar_data.shape}')
|
|
539
535
|
print('- FFT performed successfully!')
|
|
540
536
|
print_memory()
|
|
541
537
|
|
|
542
538
|
def _fft2d_numpy_efficient(self) -> None:
|
|
543
|
-
"""Perform memory-efficient 2D FFT using NumPy backend
|
|
539
|
+
"""Perform memory-efficient 2D FFT using NumPy backend with zero-padding for linear convolution.
|
|
544
540
|
|
|
545
541
|
Uses in-place operations and memory cleanup for better efficiency.
|
|
542
|
+
Pads along range axis to enable linear convolution instead of circular convolution.
|
|
546
543
|
"""
|
|
547
544
|
# Store original shape for verification
|
|
548
545
|
original_shape = self.radar_data.shape
|
|
@@ -555,51 +552,96 @@ class CoarseRDA:
|
|
|
555
552
|
print('Making data contiguous...')
|
|
556
553
|
self.radar_data = np.ascontiguousarray(self.radar_data)
|
|
557
554
|
|
|
558
|
-
#
|
|
555
|
+
# Calculate padding for linear convolution along range axis
|
|
556
|
+
len_range_line = self.radar_data.shape[1]
|
|
557
|
+
required_fft_size = len_range_line + self.num_tx_vals - 1
|
|
558
|
+
# Round up to next power of 2 for FFT efficiency
|
|
559
|
+
range_fft_size = int(2**np.ceil(np.log2(required_fft_size)))
|
|
560
|
+
|
|
561
|
+
# Safety check: ensure FFT size is at least the required size
|
|
562
|
+
assert range_fft_size >= required_fft_size, \
|
|
563
|
+
f'FFT size {range_fft_size} is smaller than required {required_fft_size}'
|
|
564
|
+
|
|
559
565
|
if self._verbose:
|
|
560
|
-
print(f'
|
|
566
|
+
print(f'Range line length: {len_range_line}')
|
|
567
|
+
print(f'TX replica length: {self.num_tx_vals}')
|
|
568
|
+
print(f'Required FFT size for linear convolution: {required_fft_size}')
|
|
569
|
+
print(f'Padded FFT size (power of 2): {range_fft_size}')
|
|
570
|
+
|
|
571
|
+
# Zero-pad along range axis to enable linear convolution
|
|
572
|
+
pad_width = range_fft_size - len_range_line
|
|
573
|
+
assert pad_width >= 0, f'Negative padding width {pad_width} calculated'
|
|
561
574
|
|
|
562
|
-
|
|
563
|
-
|
|
575
|
+
self.radar_data = np.pad(self.radar_data, ((0, 0), (0, pad_width)), mode='constant', constant_values=0)
|
|
576
|
+
|
|
577
|
+
if self._verbose:
|
|
578
|
+
print(f'Padded radar data shape: {self.radar_data.shape}')
|
|
579
|
+
|
|
580
|
+
# FFT each range line (axis=1) with explicit size for linear convolution
|
|
581
|
+
if self._verbose:
|
|
582
|
+
print(f'Performing FFT along range dimension (axis=1) with size {range_fft_size}...')
|
|
583
|
+
|
|
584
|
+
self.radar_data = np.fft.fft(self.radar_data, n=range_fft_size, axis=1)
|
|
564
585
|
|
|
565
586
|
if self._verbose:
|
|
566
587
|
print(f'First FFT along range dimension completed, shape: {self.radar_data.shape}')
|
|
567
588
|
print_memory()
|
|
568
589
|
|
|
569
|
-
# FFT each azimuth line (axis=0) with fftshift
|
|
590
|
+
# FFT each azimuth line (axis=0) with fftshift
|
|
570
591
|
if self._verbose:
|
|
571
592
|
print(f'Performing FFT along azimuth dimension (axis=0) with fftshift...')
|
|
572
593
|
|
|
573
|
-
# Use same approach as original
|
|
574
594
|
self.radar_data = np.fft.fftshift(np.fft.fft(self.radar_data, axis=0), axes=0)
|
|
575
595
|
|
|
576
596
|
if self._verbose:
|
|
577
597
|
print(f'Second FFT along azimuth dimension completed, shape: {self.radar_data.shape}')
|
|
578
598
|
print_memory()
|
|
579
|
-
|
|
580
|
-
# Verify shape preservation
|
|
581
|
-
assert self.radar_data.shape == original_shape, \
|
|
582
|
-
f'FFT changed shape from {original_shape} to {self.radar_data.shape}'
|
|
583
599
|
|
|
584
600
|
def _fft2d_torch_efficient(self) -> None:
|
|
585
|
-
"""Perform memory-efficient 2D FFT using PyTorch backend
|
|
601
|
+
"""Perform memory-efficient 2D FFT using PyTorch backend with zero-padding for linear convolution.
|
|
586
602
|
|
|
587
603
|
Uses in-place operations where possible.
|
|
604
|
+
Pads along range axis to enable linear convolution instead of circular convolution.
|
|
588
605
|
"""
|
|
589
606
|
original_shape = self.radar_data.shape
|
|
590
607
|
|
|
591
608
|
if self._verbose:
|
|
592
|
-
print('Performing memory-efficient PyTorch FFT...')
|
|
609
|
+
print('Performing memory-efficient PyTorch FFT with linear convolution padding...')
|
|
593
610
|
print_memory()
|
|
594
611
|
|
|
595
|
-
#
|
|
612
|
+
# Calculate padding for linear convolution along range axis
|
|
613
|
+
len_range_line = self.radar_data.shape[1]
|
|
614
|
+
required_fft_size = len_range_line + self.num_tx_vals - 1
|
|
615
|
+
# Round up to next power of 2 for FFT efficiency
|
|
616
|
+
range_fft_size = int(2**np.ceil(np.log2(required_fft_size)))
|
|
617
|
+
|
|
618
|
+
# Safety check: ensure FFT size is at least the required size
|
|
619
|
+
assert range_fft_size >= required_fft_size, \
|
|
620
|
+
f'FFT size {range_fft_size} is smaller than required {required_fft_size}'
|
|
621
|
+
|
|
622
|
+
if self._verbose:
|
|
623
|
+
print(f'Range line length: {len_range_line}')
|
|
624
|
+
print(f'TX replica length: {self.num_tx_vals}')
|
|
625
|
+
print(f'Required FFT size for linear convolution: {required_fft_size}')
|
|
626
|
+
print(f'Padded FFT size (power of 2): {range_fft_size}')
|
|
627
|
+
|
|
628
|
+
# Zero-pad along range axis
|
|
629
|
+
pad_width = range_fft_size - len_range_line
|
|
630
|
+
assert pad_width >= 0, f'Negative padding width {pad_width} calculated'
|
|
631
|
+
|
|
632
|
+
self.radar_data = torch.nn.functional.pad(self.radar_data, (0, pad_width), mode='constant', value=0)
|
|
633
|
+
|
|
634
|
+
if self._verbose:
|
|
635
|
+
print(f'Padded radar data shape: {self.radar_data.shape}')
|
|
636
|
+
|
|
637
|
+
# FFT each range line (axis=1) with explicit size - in-place when possible
|
|
596
638
|
if self._memory_efficient:
|
|
597
|
-
temp = torch.fft.fft(self.radar_data, dim=1)
|
|
639
|
+
temp = torch.fft.fft(self.radar_data, n=range_fft_size, dim=1)
|
|
598
640
|
self.radar_data.copy_(temp)
|
|
599
641
|
del temp
|
|
600
642
|
torch.cuda.empty_cache() if torch.cuda.is_available() else None
|
|
601
643
|
else:
|
|
602
|
-
self.radar_data = torch.fft.fft(self.radar_data, dim=1)
|
|
644
|
+
self.radar_data = torch.fft.fft(self.radar_data, n=range_fft_size, dim=1)
|
|
603
645
|
|
|
604
646
|
# FFT each azimuth line (axis=0) with fftshift
|
|
605
647
|
if self._memory_efficient:
|
|
@@ -616,21 +658,29 @@ class CoarseRDA:
|
|
|
616
658
|
torch.fft.fft(self.radar_data, dim=0),
|
|
617
659
|
dim=0
|
|
618
660
|
)
|
|
619
|
-
|
|
620
|
-
# Verify shape preservation
|
|
621
|
-
assert self.radar_data.shape == original_shape, \
|
|
622
|
-
f'Torch FFT changed shape from {original_shape} to {self.radar_data.shape}'
|
|
623
661
|
|
|
624
662
|
@flush_mem
|
|
625
663
|
@timing_decorator
|
|
626
664
|
def ifft_range(self) -> None:
|
|
627
|
-
"""Perform memory-efficient inverse FFT along range dimension."""
|
|
665
|
+
"""Perform memory-efficient inverse FFT along range dimension and trim to original size."""
|
|
628
666
|
if self._backend == 'numpy':
|
|
629
|
-
#
|
|
667
|
+
# Inverse FFT along range with the padded size
|
|
630
668
|
self.radar_data = np.fft.ifftshift(np.fft.ifft(self.radar_data, axis=1), axes=1)
|
|
669
|
+
|
|
670
|
+
# Trim back to original range dimension (linear convolution complete)
|
|
671
|
+
self.radar_data = self.radar_data[:, :self.len_range_line]
|
|
672
|
+
|
|
673
|
+
if self._verbose:
|
|
674
|
+
print(f'Trimmed radar data back to original range dimension: {self.radar_data.shape}')
|
|
631
675
|
elif self._backend == 'torch':
|
|
632
676
|
self.radar_data = torch.fft.ifft(self.radar_data, dim=1)
|
|
633
677
|
self.radar_data = torch.fft.ifftshift(self.radar_data, dim=1)
|
|
678
|
+
|
|
679
|
+
# Trim back to original range dimension
|
|
680
|
+
self.radar_data = self.radar_data[:, :self.len_range_line]
|
|
681
|
+
|
|
682
|
+
if self._verbose:
|
|
683
|
+
print(f'Trimmed radar data back to original range dimension: {self.radar_data.shape}')
|
|
634
684
|
else:
|
|
635
685
|
raise ValueError(f'Unsupported backend: {self._backend}')
|
|
636
686
|
|
|
@@ -1379,10 +1429,10 @@ class CoarseRDA:
|
|
|
1379
1429
|
print(f'Raw radar data shape: {self.raw_data.shape}')
|
|
1380
1430
|
print_memory()
|
|
1381
1431
|
# ------------------------------------------------------------------------
|
|
1382
|
-
# Step 1: 2D FFT transformation (
|
|
1432
|
+
# Step 1: 2D FFT transformation (pads range dimension for linear convolution)
|
|
1383
1433
|
self.fft2d()
|
|
1384
|
-
|
|
1385
|
-
f'FFT
|
|
1434
|
+
if self._verbose:
|
|
1435
|
+
print(f'FFT completed, radar data shape (padded): {self.radar_data.shape}')
|
|
1386
1436
|
# ------------------------------------------------------------------------
|
|
1387
1437
|
|
|
1388
1438
|
# Step 2: Range compression
|
|
@@ -1414,7 +1464,7 @@ class CoarseRDA:
|
|
|
1414
1464
|
"""Perform memory-efficient range compression step.
|
|
1415
1465
|
|
|
1416
1466
|
This method applies the range compression filter to compress the radar
|
|
1417
|
-
signal in the range dimension
|
|
1467
|
+
signal in the range dimension. Range dimension is padded for linear convolution.
|
|
1418
1468
|
|
|
1419
1469
|
Raises:
|
|
1420
1470
|
RuntimeError: If data dimensions change unexpectedly during processing.
|
|
@@ -1432,14 +1482,14 @@ class CoarseRDA:
|
|
|
1432
1482
|
original_w = initial_shape[1]
|
|
1433
1483
|
|
|
1434
1484
|
if self._verbose:
|
|
1435
|
-
print(f'Processing with
|
|
1485
|
+
print(f'Processing with padded range dimension={original_w}')
|
|
1436
1486
|
|
|
1437
1487
|
# Perform range compression
|
|
1438
1488
|
self._perform_range_compression_efficient(w_pad, original_w)
|
|
1439
1489
|
|
|
1440
|
-
# Verify
|
|
1441
|
-
assert self.radar_data.shape == initial_shape, \
|
|
1442
|
-
f'Range compression changed
|
|
1490
|
+
# Verify azimuth dimension is preserved (range may be padded)
|
|
1491
|
+
assert self.radar_data.shape[0] == initial_shape[0], \
|
|
1492
|
+
f'Range compression changed azimuth dimension from {initial_shape[0]} to {self.radar_data.shape[0]}'
|
|
1443
1493
|
|
|
1444
1494
|
if self._verbose:
|
|
1445
1495
|
print(f'Range compression completed successfully!')
|
|
@@ -1452,7 +1502,7 @@ class CoarseRDA:
|
|
|
1452
1502
|
"""Perform memory-efficient Range Cell Migration Correction.
|
|
1453
1503
|
|
|
1454
1504
|
This method applies the RCMC filter to correct for range cell migration
|
|
1455
|
-
effects and performs inverse FFT in the range dimension.
|
|
1505
|
+
effects and performs inverse FFT in the range dimension (trimming back to original size).
|
|
1456
1506
|
|
|
1457
1507
|
Raises:
|
|
1458
1508
|
RuntimeError: If data dimensions change unexpectedly during processing.
|
|
@@ -1462,15 +1512,13 @@ class CoarseRDA:
|
|
|
1462
1512
|
print(f'Input radar data shape: {self.radar_data.shape}')
|
|
1463
1513
|
print_memory()
|
|
1464
1514
|
|
|
1465
|
-
#
|
|
1466
|
-
initial_shape = self.radar_data.shape
|
|
1467
|
-
|
|
1468
|
-
# Perform RCMC
|
|
1515
|
+
# Perform RCMC (includes ifft_range which trims to original size)
|
|
1469
1516
|
self._perform_rcmc_efficient()
|
|
1470
1517
|
|
|
1471
|
-
#
|
|
1472
|
-
|
|
1473
|
-
|
|
1518
|
+
# After RCMC and ifft_range, data should be back to original dimensions
|
|
1519
|
+
expected_shape = (self.len_az_line, self.len_range_line)
|
|
1520
|
+
assert self.radar_data.shape == expected_shape, \
|
|
1521
|
+
f'RCMC resulted in unexpected shape {self.radar_data.shape}, expected {expected_shape}'
|
|
1474
1522
|
|
|
1475
1523
|
if self._verbose:
|
|
1476
1524
|
print(f'RCMC completed successfully!')
|
|
@@ -1495,6 +1543,11 @@ class CoarseRDA:
|
|
|
1495
1543
|
|
|
1496
1544
|
# Store initial shape for verification
|
|
1497
1545
|
initial_shape = self.radar_data.shape
|
|
1546
|
+
expected_shape = (self.len_az_line, self.len_range_line)
|
|
1547
|
+
|
|
1548
|
+
# Verify we have original dimensions after RCMC
|
|
1549
|
+
assert initial_shape == expected_shape, \
|
|
1550
|
+
f'Unexpected input shape {initial_shape}, expected {expected_shape}'
|
|
1498
1551
|
|
|
1499
1552
|
# Perform azimuth compression
|
|
1500
1553
|
self._perform_azimuth_compression_efficient()
|
|
@@ -1509,43 +1562,37 @@ class CoarseRDA:
|
|
|
1509
1562
|
print_memory()
|
|
1510
1563
|
|
|
1511
1564
|
def _perform_range_compression_efficient(self, w_pad: int, original_w: int) -> None:
|
|
1512
|
-
"""Perform memory-efficient range compression step
|
|
1565
|
+
"""Perform memory-efficient range compression step with padded dimensions.
|
|
1513
1566
|
|
|
1514
1567
|
Args:
|
|
1515
|
-
w_pad: Width padding for
|
|
1516
|
-
original_w: Original width
|
|
1568
|
+
w_pad: Width padding (unused, kept for interface compatibility).
|
|
1569
|
+
original_w: Original padded width from FFT.
|
|
1517
1570
|
|
|
1518
1571
|
Raises:
|
|
1519
1572
|
ValueError: If array shapes are incompatible.
|
|
1520
|
-
AssertionError: If dimensions change unexpectedly.
|
|
1521
1573
|
"""
|
|
1522
1574
|
if self._verbose:
|
|
1523
1575
|
print(f'Starting memory-efficient range compression...')
|
|
1524
|
-
print(f'Radar data shape: {self.radar_data.shape}')
|
|
1576
|
+
print(f'Radar data shape (padded): {self.radar_data.shape}')
|
|
1525
1577
|
print_memory()
|
|
1526
1578
|
|
|
1527
1579
|
# Store original shape for verification
|
|
1528
1580
|
original_shape = self.radar_data.shape
|
|
1529
|
-
expected_shape = (self.len_az_line, self.len_range_line)
|
|
1530
|
-
|
|
1531
|
-
# Verify we still have expected dimensions
|
|
1532
|
-
assert original_shape == expected_shape, \
|
|
1533
|
-
f'Unexpected radar data shape: {original_shape}, expected: {expected_shape}'
|
|
1534
1581
|
|
|
1535
|
-
# Get range filter with matching dimensions
|
|
1582
|
+
# Get range filter with matching padded dimensions
|
|
1536
1583
|
range_filter = self.get_range_filter()
|
|
1537
1584
|
|
|
1538
1585
|
if self._verbose:
|
|
1539
1586
|
print(f'Range filter shape: {range_filter.shape}')
|
|
1540
1587
|
print(f'Applying range compression filter...')
|
|
1541
1588
|
|
|
1542
|
-
# Apply range compression filter
|
|
1589
|
+
# Apply range compression filter
|
|
1543
1590
|
self.radar_data = multiply(self.radar_data, range_filter)
|
|
1544
1591
|
|
|
1545
1592
|
# Cleanup filter
|
|
1546
1593
|
cleanup_variables(range_filter)
|
|
1547
1594
|
|
|
1548
|
-
# Verify dimensions are preserved
|
|
1595
|
+
# Verify dimensions are preserved during multiplication
|
|
1549
1596
|
assert self.radar_data.shape == original_shape, \
|
|
1550
1597
|
f'Range compression changed data shape from {original_shape} to {self.radar_data.shape}'
|
|
1551
1598
|
|
sarpyx/science/__init__.py
CHANGED
sarpyx/sla/__init__.py
CHANGED
|
@@ -6,12 +6,20 @@ including handler utilities and analysis tools.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .core import SubLookAnalysis, Handler
|
|
9
|
+
from . import metrics
|
|
10
|
+
from .metrics import enl, interlook_coherence, dispersion_ratio, phase_variance, stack_metrics
|
|
9
11
|
# Import utility functions if they exist in utilis.py
|
|
10
12
|
# from .utilis import delete, unzip, delProd, command_line, iterNodes
|
|
11
13
|
|
|
12
14
|
__all__ = [
|
|
13
15
|
'SubLookAnalysis',
|
|
14
16
|
'Handler',
|
|
17
|
+
'metrics',
|
|
18
|
+
'enl',
|
|
19
|
+
'interlook_coherence',
|
|
20
|
+
'dispersion_ratio',
|
|
21
|
+
'phase_variance',
|
|
22
|
+
'stack_metrics',
|
|
15
23
|
# Uncomment these when utilis.py functions are properly imported
|
|
16
24
|
# 'delete',
|
|
17
25
|
# 'unzip',
|
sarpyx/sla/metrics.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Sub-look metrics.
|
|
2
|
+
|
|
3
|
+
Example:
|
|
4
|
+
from srp.sarpyx.sla.metrics import stack_metrics
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _intensity(x: np.ndarray) -> np.ndarray:
|
|
13
|
+
return np.abs(x) ** 2 if np.iscomplexobj(x) else np.asarray(x)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def enl(x: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
|
|
17
|
+
"""Computes equivalent number of looks (ENL).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
x (np.ndarray): Intensity or complex samples.
|
|
21
|
+
axis: Axis of looks.
|
|
22
|
+
eps (float): Small value to avoid division by zero.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
np.ndarray: ENL estimate.
|
|
26
|
+
"""
|
|
27
|
+
i = _intensity(x)
|
|
28
|
+
m = np.mean(i, axis=axis)
|
|
29
|
+
v = np.var(i, axis=axis)
|
|
30
|
+
return (m * m) / (v + eps)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def dispersion_ratio(x: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
|
|
34
|
+
"""Computes dispersion ratio (normalized variance).
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
x (np.ndarray): Intensity or complex samples.
|
|
38
|
+
axis: Axis of looks.
|
|
39
|
+
eps (float): Small value to avoid division by zero.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
np.ndarray: Dispersion ratio.
|
|
43
|
+
"""
|
|
44
|
+
i = _intensity(x)
|
|
45
|
+
m = np.mean(i, axis=axis)
|
|
46
|
+
v = np.var(i, axis=axis)
|
|
47
|
+
return v / (m * m + eps)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def interlook_coherence(a: np.ndarray, b: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
|
|
51
|
+
"""Computes inter-look coherence between two complex looks.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
a (np.ndarray): First complex look.
|
|
55
|
+
b (np.ndarray): Second complex look.
|
|
56
|
+
axis: Averaging axis.
|
|
57
|
+
eps (float): Small value to avoid division by zero.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
np.ndarray: Coherence magnitude in [0, 1].
|
|
61
|
+
"""
|
|
62
|
+
num = np.abs(np.mean(a * np.conj(b), axis=axis))
|
|
63
|
+
den = np.sqrt(np.mean(np.abs(a) ** 2, axis=axis) * np.mean(np.abs(b) ** 2, axis=axis))
|
|
64
|
+
return num / (den + eps)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def phase_variance(x: np.ndarray, axis=None) -> np.ndarray:
|
|
68
|
+
"""Computes circular phase variance.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
x (np.ndarray): Complex samples.
|
|
72
|
+
axis: Axis of looks.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
np.ndarray: Circular phase variance in [0, 1].
|
|
76
|
+
"""
|
|
77
|
+
ph = np.angle(x)
|
|
78
|
+
return 1.0 - np.abs(np.mean(np.exp(1j * ph), axis=axis))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def stack_metrics(stack: np.ndarray, look_axis: int = 0, pair=(0, 1), eps: float = 1e-12):
|
|
82
|
+
"""Computes all metrics from a sub-look stack.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
stack (np.ndarray): Complex sub-look stack.
|
|
86
|
+
look_axis (int): Axis of looks.
|
|
87
|
+
pair (tuple[int, int]): Look indices for coherence.
|
|
88
|
+
eps (float): Small value to avoid division by zero.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: ENL, coherence,
|
|
92
|
+
dispersion ratio, phase variance.
|
|
93
|
+
"""
|
|
94
|
+
s = np.moveaxis(stack, look_axis, 0)
|
|
95
|
+
a, b = s[pair[0]], s[pair[1]]
|
|
96
|
+
return (
|
|
97
|
+
enl(s, axis=0, eps=eps),
|
|
98
|
+
interlook_coherence(a, b, axis=0, eps=eps),
|
|
99
|
+
dispersion_ratio(s, axis=0, eps=eps),
|
|
100
|
+
phase_variance(s, axis=0),
|
|
101
|
+
)
|