Initial commit: Speckle-Scanner 3D pipeline with setup README
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
# 04 Rectification
|
||||
|
||||
Stereo rectification for multi-camera scan sessions. Reads raw images from `3D-Scans`, applies calibration from `Calib-data`, and writes results into `Speckle-Scanner_Processing_data`.
|
||||
|
||||
Supported stereo pairs (per scan):
|
||||
|
||||
| Pair | Left | Right | Params file |
|
||||
|------|------|-------|-------------|
|
||||
| `lc-rc` | `lc_*` | `rc_*` | `lc-rc_parameters.npz` |
|
||||
| `lc-rg` | `lc_*` | `rg_*` | `lc-rg_parameters.npz` |
|
||||
| `lc-ir` | `lc_*` | `ir_*` | `lc-ir_parameters.npz` |
|
||||
|
||||
Rectified LC frames are taken from the `lc-rc` run only (one LC set in `02_rect_images`). Partner cameras (`rc`, `rg`, `ir`) are saved from their own pair calibration.
|
||||
|
||||
---
|
||||
|
||||
## Folder layout (general paths)
|
||||
|
||||
All paths use `$HOME` — replace with your home directory on any machine.
|
||||
|
||||
| Role | Path pattern |
|
||||
|------|----------------|
|
||||
| Source scans (RAW) | `$HOME/3D-Scans/<raw_project>/<date>/sessionN/Scan00000X/01_raw_images/` |
|
||||
| Calibration params | `$HOME/Calib-data/<project>/<date>/<calib_name>/params/` |
|
||||
| Processing output | `$HOME/Speckle-Scanner_Processing_data/<project>/<date>/` |
|
||||
|
||||
Example naming:
|
||||
|
||||
- **project** (Calib + processing): `Olsen_wings` (underscore)
|
||||
- **raw_project** (3D-Scans): `Olsen-wings` (often hyphen; default = project with `_` → `-`)
|
||||
- **date**: `2026-05-12`
|
||||
- **calib_name**: `calib1`
|
||||
|
||||
Per session under processing output:
|
||||
|
||||
```text
|
||||
$HOME/Speckle-Scanner_Processing_data/<project>/<date>/
|
||||
session53/
|
||||
params_link/ # copied lc-rc, lc-rg, lc-ir params
|
||||
Scan000001/
|
||||
01_raw_images/ # copy of source images
|
||||
02_rect_images/ # rectified lc_*, rc_*, rg_*, ir_* (single folder)
|
||||
Scan000002/
|
||||
...
|
||||
```
|
||||
|
||||
Source side (same session/scan names):
|
||||
|
||||
```text
|
||||
$HOME/3D-Scans/<raw_project>/<date>/session53/Scan000001/01_raw_images/
|
||||
```
|
||||
|
||||
Calibration params (once per project/date):
|
||||
|
||||
```text
|
||||
$HOME/Calib-data/<project>/<date>/<calib_name>/params/
|
||||
lc-rc_parameters.npz
|
||||
lc-rc_stereo_cam_model.yaml
|
||||
lc-rc_Q.cvstore
|
||||
lc-rg_*
|
||||
lc-ir_*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
```bash
|
||||
pip install numpy opencv-python tqdm
|
||||
```
|
||||
|
||||
Use a Python environment where `import cv2` works.
|
||||
|
||||
---
|
||||
|
||||
## How to run
|
||||
|
||||
From anywhere:
|
||||
|
||||
```bash
|
||||
cd "$HOME/Speckle-Scanner/04_Rectification"
|
||||
python main.py [options]
|
||||
```
|
||||
|
||||
### All sessions under one date (full batch)
|
||||
|
||||
Processes every `session*/Scan*/01_raw_images` under the date folder.
|
||||
|
||||
```bash
|
||||
python main.py \
|
||||
--project Olsen_wings \
|
||||
--raw_project Olsen-wings \
|
||||
--date 2026-05-12 \
|
||||
--calib_name calib1
|
||||
```
|
||||
|
||||
One line:
|
||||
|
||||
```bash
|
||||
python main.py --project Olsen_wings --raw_project Olsen-wings --date 2026-05-12 --calib_name calib1
|
||||
```
|
||||
|
||||
### One session only
|
||||
|
||||
```bash
|
||||
python main.py \
|
||||
--project Olsen_wings \
|
||||
--raw_project Olsen-wings \
|
||||
--date 2026-05-12 \
|
||||
--calib_name calib1 \
|
||||
--session session53
|
||||
```
|
||||
|
||||
### Custom pairs
|
||||
|
||||
Default: `lc-rc,lc-rg,lc-ir`. Example — RC and IR only:
|
||||
|
||||
```bash
|
||||
python main.py --project Olsen_wings --date 2026-05-12 --pairs lc-rc,lc-ir
|
||||
```
|
||||
|
||||
### Override paths (any project/machine)
|
||||
|
||||
```bash
|
||||
python main.py \
|
||||
--source_date_root "$HOME/3D-Scans/MyProject/2026-05-12" \
|
||||
--calib_params_dir "$HOME/Calib-data/MyProject/2026-05-12/calib1/params" \
|
||||
--processing_date_root "$HOME/Speckle-Scanner_Processing_data/MyProject/2026-05-12"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI reference
|
||||
|
||||
| Option | Default | Meaning |
|
||||
|--------|---------|---------|
|
||||
| `--project` | **required** | Project name in Calib-data and Processing_data (e.g. `Olsen_wings`) |
|
||||
| `--date` | **required** | Date subfolder (e.g. `2026-05-12`) |
|
||||
| `--raw_project` | `<project>` with `_` → `-` | Project folder name under 3D-Scans |
|
||||
| `--session` | (all) | Only this session, e.g. `session53` |
|
||||
| `--calib_name` | `calib1` | Calibration run folder |
|
||||
| `--pairs` | `lc-rc,lc-rg,lc-ir` | Comma-separated stereo pairs |
|
||||
| `--keep_lc_from_pair` | `lc-rc` | Which pair defines rectified LC in `02_rect_images` |
|
||||
| `--source_date_root` | auto | Override RAW scan root |
|
||||
| `--calib_params_dir` | auto | Override params folder |
|
||||
| `--processing_date_root` | auto | Override output root |
|
||||
|
||||
---
|
||||
|
||||
## Pairing notes
|
||||
|
||||
Images are matched by filename key (in order):
|
||||
|
||||
1. `_ts<number>` in both names (e.g. `lc_ts254303092_...` ↔ `rc_ts254303092_...`)
|
||||
2. `scan000001` style / `IR_scan_000001`
|
||||
3. Prefix + suffix (`lc_123` ↔ `ir_123`)
|
||||
|
||||
If no key match for `lc-rg` or `lc-ir`, the script may use **index fallback** (first LC with first RG/IR). Check logs for:
|
||||
|
||||
```text
|
||||
[WARN] No key match for lc-rg; using index fallback with N pairs.
|
||||
```
|
||||
|
||||
`lc-rc` usually matches on `_ts` when both cameras captured the same timestamps.
|
||||
|
||||
---
|
||||
|
||||
## What gets created
|
||||
|
||||
For each processed scan:
|
||||
|
||||
- Copies `01_raw_images` into processing tree (does not delete source RAW data)
|
||||
- Writes rectified images to `02_rect_images/`
|
||||
- Creates `params_link/` once per session with all calibration files
|
||||
|
||||
Does **not** modify files under `3D-Scans`.
|
||||
|
||||
---
|
||||
|
||||
## Quick check after a run
|
||||
|
||||
```bash
|
||||
PROJECT=Olsen_wings
|
||||
DATE=2026-05-12
|
||||
SESSION=session53
|
||||
SCAN=Scan000001
|
||||
|
||||
ls "$HOME/Speckle-Scanner_Processing_data/$PROJECT/$DATE/$SESSION/params_link"
|
||||
ls "$HOME/Speckle-Scanner_Processing_data/$PROJECT/$DATE/$SESSION/$SCAN/02_rect_images" | head
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
```bash
|
||||
# This step only
|
||||
pip install -r ~/Speckle-Scanner/04_Rectification/requirements.txt
|
||||
|
||||
# Or install everything for the full pipeline
|
||||
pip install -r ~/Speckle-Scanner/requirements.txt
|
||||
```
|
||||
|
||||
Packages: `numpy`, `opencv-python`, `tqdm`.
|
||||
@@ -0,0 +1,85 @@
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
|
||||
from rectificationclasses.rectification import Rectification
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Batch stereo rectification")
|
||||
parser.add_argument("--project", type=str, required=True, help="Project name used for Calib-data and processing_data (e.g. Olsen_wings)")
|
||||
parser.add_argument(
|
||||
"--raw_project",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Project name used in 3D-Scans (default: project with '_' replaced by '-')",
|
||||
)
|
||||
parser.add_argument("--date", type=str, required=True, help="Date folder (e.g. 2026-05-12)")
|
||||
parser.add_argument(
|
||||
"--session",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Process only this session folder (e.g. session53). Default: all sessions under the date.",
|
||||
)
|
||||
parser.add_argument("--calib_name", type=str, default="calib1", help="Calibration folder under Calib-data/<project>/<date>/")
|
||||
parser.add_argument(
|
||||
"--pairs",
|
||||
type=str,
|
||||
default="lc-rc,lc-rg,lc-ir",
|
||||
help="Comma-separated pair list, e.g. lc-rc,lc-rg,lc-ir",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--keep_lc_from_pair",
|
||||
type=str,
|
||||
default="lc-rc",
|
||||
help="Pair whose rectified LC frames are kept in 02_rect_images.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--source_date_root",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Override source root (default: ~/3D-Scans/<raw_project>/<date>)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--calib_params_dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Override calib params dir (default: ~/Calib-data/<project>/<date>/<calib_name>/params)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--processing_date_root",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Override processing target root (default: ~/Speckle-Scanner_Processing_data/<project>/<date>)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
home = Path.home()
|
||||
raw_project = args.raw_project or args.project.replace("_", "-")
|
||||
pairs = tuple([p.strip() for p in args.pairs.split(",") if p.strip()])
|
||||
|
||||
source_date_root = Path(args.source_date_root) if args.source_date_root else (
|
||||
home / "3D-Scans" / raw_project / args.date
|
||||
)
|
||||
calib_params_dir = Path(args.calib_params_dir) if args.calib_params_dir else (
|
||||
home / "Calib-data" / args.project / args.date / args.calib_name / "params"
|
||||
)
|
||||
processing_date_root = Path(args.processing_date_root) if args.processing_date_root else (
|
||||
home / "Speckle-Scanner_Processing_data" / args.project / args.date
|
||||
)
|
||||
|
||||
rectificator = Rectification(
|
||||
source_date_root=str(source_date_root),
|
||||
calib_params_dir=str(calib_params_dir),
|
||||
processing_date_root=str(processing_date_root),
|
||||
pairs=pairs,
|
||||
keep_lc_from_pair=args.keep_lc_from_pair,
|
||||
session_filter=args.session,
|
||||
)
|
||||
rectificator.run_batch()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,333 @@
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
VALID_EXTS = {".bmp", ".png", ".jpg", ".jpeg"}
|
||||
|
||||
|
||||
class Rectification:
|
||||
"""Batch rectification for one project/date tree.
|
||||
|
||||
Reads source scans from RAW data tree, copies scans into processing tree, and
|
||||
rectifies lc-rc/lc-rg/lc-ir pairs with pair-specific calibration params.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source_date_root: str,
|
||||
calib_params_dir: str,
|
||||
processing_date_root: str,
|
||||
pairs: Tuple[str, ...] = ("lc-rc", "lc-rg", "lc-ir"),
|
||||
keep_lc_from_pair: str = "lc-rc",
|
||||
session_filter: Optional[str] = None,
|
||||
) -> None:
|
||||
self.source_date_root = Path(source_date_root)
|
||||
self.calib_params_dir = Path(calib_params_dir)
|
||||
self.processing_date_root = Path(processing_date_root)
|
||||
self.pairs = pairs
|
||||
self.keep_lc_from_pair = keep_lc_from_pair
|
||||
self.session_filter = session_filter
|
||||
|
||||
if not self.source_date_root.is_dir():
|
||||
raise FileNotFoundError(f"Source date root not found: {self.source_date_root}")
|
||||
if not self.calib_params_dir.is_dir():
|
||||
raise FileNotFoundError(f"Calibration params dir not found: {self.calib_params_dir}")
|
||||
|
||||
self.processing_date_root.mkdir(parents=True, exist_ok=True)
|
||||
self._params_by_pair: Dict[str, Dict[str, np.ndarray]] = {}
|
||||
self._rect_maps_cache: Dict[Tuple[str, int, int], Tuple[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray], np.ndarray]] = {}
|
||||
|
||||
self._load_all_pair_params()
|
||||
|
||||
@staticmethod
|
||||
def _extract_ts_key(filename: str) -> Optional[str]:
|
||||
stem = Path(filename).stem.lower()
|
||||
m = re.search(r"_ts(\d+)", stem)
|
||||
return m.group(1) if m else None
|
||||
|
||||
@staticmethod
|
||||
def _extract_scan_key(filename: str) -> Optional[str]:
|
||||
stem = Path(filename).stem.lower()
|
||||
m = re.search(r"(scan\d{6})", stem)
|
||||
if m:
|
||||
return m.group(1)
|
||||
m = re.match(r"^ir_scan_(\d+)", stem)
|
||||
if m:
|
||||
return f"scan{int(m.group(1)):06d}"
|
||||
m = re.match(r"^ir_(\d{6})(?:_|$)", stem)
|
||||
if m:
|
||||
return f"scan{m.group(1)}"
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_generic_suffix_key(filename: str, prefix: str) -> Optional[str]:
|
||||
stem = Path(filename).stem.lower()
|
||||
if not stem.startswith(prefix):
|
||||
return None
|
||||
return stem[len(prefix):].lstrip("_-.")
|
||||
|
||||
@staticmethod
|
||||
def _camera_from_pair(pair_name: str) -> str:
|
||||
return pair_name.split("-", 1)[1]
|
||||
|
||||
def _load_pair_params(self, pair_name: str) -> Dict[str, np.ndarray]:
|
||||
npz_path = self.calib_params_dir / f"{pair_name}_parameters.npz"
|
||||
if not npz_path.exists():
|
||||
raise FileNotFoundError(f"Missing params file for {pair_name}: {npz_path}")
|
||||
data = np.load(npz_path, allow_pickle=True)
|
||||
params = dict(data)
|
||||
required = [
|
||||
"L_Intrinsic",
|
||||
"L_Distortion",
|
||||
"R_Intrinsic",
|
||||
"R_Distortion",
|
||||
"Rotation",
|
||||
"Translation",
|
||||
]
|
||||
missing = [k for k in required if k not in params]
|
||||
if missing:
|
||||
raise KeyError(f"{pair_name} params missing keys: {missing}")
|
||||
return params
|
||||
|
||||
def _load_all_pair_params(self) -> None:
|
||||
for pair_name in self.pairs:
|
||||
self._params_by_pair[pair_name] = self._load_pair_params(pair_name)
|
||||
print(f"[INFO] Loaded calibration params for pairs: {', '.join(self.pairs)}")
|
||||
|
||||
def _copy_params_link_for_session(self, session_name: str) -> None:
|
||||
target_params = self.processing_date_root / session_name / "params_link"
|
||||
target_params.mkdir(parents=True, exist_ok=True)
|
||||
for src in self.calib_params_dir.iterdir():
|
||||
if src.is_file() and src.suffix.lower() in (".npz", ".yaml", ".cvstore"):
|
||||
shutil.copy2(src, target_params / src.name)
|
||||
|
||||
@staticmethod
|
||||
def _copy_raw_images(src_raw_dir: Path, dst_raw_dir: Path) -> None:
|
||||
dst_raw_dir.mkdir(parents=True, exist_ok=True)
|
||||
for src in src_raw_dir.iterdir():
|
||||
if src.is_file():
|
||||
shutil.copy2(src, dst_raw_dir / src.name)
|
||||
|
||||
@staticmethod
|
||||
def _list_images(raw_dir: Path, prefix: str) -> List[Path]:
|
||||
imgs = [
|
||||
p for p in raw_dir.iterdir()
|
||||
if p.is_file()
|
||||
and p.suffix.lower() in VALID_EXTS
|
||||
and p.name.lower().startswith(prefix.lower())
|
||||
]
|
||||
imgs.sort()
|
||||
return imgs
|
||||
|
||||
def _pair_images(self, left_images: List[Path], right_images: List[Path], right_camera: str) -> List[Tuple[Path, Path]]:
|
||||
left_by_ts = {self._extract_ts_key(p.name): p for p in left_images if self._extract_ts_key(p.name)}
|
||||
right_by_ts = {self._extract_ts_key(p.name): p for p in right_images if self._extract_ts_key(p.name)}
|
||||
pairs: List[Tuple[Path, Path]] = []
|
||||
|
||||
common_ts = sorted(set(left_by_ts.keys()) & set(right_by_ts.keys()))
|
||||
for ts in common_ts:
|
||||
pairs.append((left_by_ts[ts], right_by_ts[ts]))
|
||||
if pairs:
|
||||
return pairs
|
||||
|
||||
left_by_scan = {self._extract_scan_key(p.name): p for p in left_images if self._extract_scan_key(p.name)}
|
||||
right_by_scan = {self._extract_scan_key(p.name): p for p in right_images if self._extract_scan_key(p.name)}
|
||||
common_scan = sorted(set(left_by_scan.keys()) & set(right_by_scan.keys()))
|
||||
for skey in common_scan:
|
||||
pairs.append((left_by_scan[skey], right_by_scan[skey]))
|
||||
if pairs:
|
||||
return pairs
|
||||
|
||||
left_by_suffix = {
|
||||
self._extract_generic_suffix_key(p.name, "lc"): p
|
||||
for p in left_images
|
||||
if self._extract_generic_suffix_key(p.name, "lc")
|
||||
}
|
||||
right_by_suffix = {
|
||||
self._extract_generic_suffix_key(p.name, right_camera): p
|
||||
for p in right_images
|
||||
if self._extract_generic_suffix_key(p.name, right_camera)
|
||||
}
|
||||
common_suffix = sorted(set(left_by_suffix.keys()) & set(right_by_suffix.keys()))
|
||||
for key in common_suffix:
|
||||
pairs.append((left_by_suffix[key], right_by_suffix[key]))
|
||||
if pairs:
|
||||
return pairs
|
||||
|
||||
fallback_count = min(len(left_images), len(right_images))
|
||||
if fallback_count > 0:
|
||||
print(
|
||||
f"[WARN] No key match for lc-{right_camera}; "
|
||||
f"using index fallback with {fallback_count} pairs."
|
||||
)
|
||||
return list(zip(left_images[:fallback_count], right_images[:fallback_count]))
|
||||
return []
|
||||
|
||||
def _get_rectification_maps(
|
||||
self,
|
||||
pair_name: str,
|
||||
left_size: Tuple[int, int],
|
||||
right_size: Tuple[int, int],
|
||||
) -> Tuple[Tuple[np.ndarray, np.ndarray], Tuple[np.ndarray, np.ndarray], np.ndarray]:
|
||||
cache_key = (pair_name, left_size[0], left_size[1])
|
||||
if cache_key in self._rect_maps_cache:
|
||||
return self._rect_maps_cache[cache_key]
|
||||
|
||||
params = self._params_by_pair[pair_name]
|
||||
rect_left, rect_right, proj_left, proj_right, q_mat, _, _ = cv2.stereoRectify(
|
||||
params["L_Intrinsic"],
|
||||
params["L_Distortion"],
|
||||
params["R_Intrinsic"],
|
||||
params["R_Distortion"],
|
||||
left_size,
|
||||
params["Rotation"],
|
||||
params["Translation"],
|
||||
alpha=1,
|
||||
flags=0,
|
||||
)
|
||||
|
||||
left_maps = cv2.initUndistortRectifyMap(
|
||||
params["L_Intrinsic"],
|
||||
params["L_Distortion"],
|
||||
rect_left,
|
||||
proj_left,
|
||||
left_size,
|
||||
cv2.CV_32FC1,
|
||||
)
|
||||
right_maps = cv2.initUndistortRectifyMap(
|
||||
params["R_Intrinsic"],
|
||||
params["R_Distortion"],
|
||||
rect_right,
|
||||
proj_right,
|
||||
right_size,
|
||||
cv2.CV_32FC1,
|
||||
)
|
||||
self._rect_maps_cache[cache_key] = (left_maps, right_maps, q_mat)
|
||||
return left_maps, right_maps, q_mat
|
||||
|
||||
def _rectify_pair_image(
|
||||
self,
|
||||
pair_name: str,
|
||||
left_img: np.ndarray,
|
||||
right_img: np.ndarray,
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
left_size = (left_img.shape[1], left_img.shape[0])
|
||||
right_size = (right_img.shape[1], right_img.shape[0])
|
||||
left_maps, right_maps, _ = self._get_rectification_maps(pair_name, left_size, right_size)
|
||||
left_rect = cv2.remap(left_img, left_maps[0], left_maps[1], cv2.INTER_AREA)
|
||||
right_rect = cv2.remap(right_img, right_maps[0], right_maps[1], cv2.INTER_AREA)
|
||||
return left_rect, right_rect
|
||||
|
||||
def _process_scan(self, session_name: str, scan_name: str) -> Dict[str, int]:
|
||||
src_raw_dir = self.source_date_root / session_name / scan_name / "01_raw_images"
|
||||
dst_scan_dir = self.processing_date_root / session_name / scan_name
|
||||
dst_raw_dir = dst_scan_dir / "01_raw_images"
|
||||
dst_rect_dir = dst_scan_dir / "02_rect_images"
|
||||
dst_rect_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self._copy_raw_images(src_raw_dir, dst_raw_dir)
|
||||
|
||||
stats = {"pairs_total": 0, "saved": 0, "skipped": 0}
|
||||
lc_written = False
|
||||
ordered_pairs = list(self.pairs)
|
||||
if self.keep_lc_from_pair in ordered_pairs:
|
||||
ordered_pairs.remove(self.keep_lc_from_pair)
|
||||
ordered_pairs.insert(0, self.keep_lc_from_pair)
|
||||
|
||||
for pair_name in ordered_pairs:
|
||||
right_camera = self._camera_from_pair(pair_name)
|
||||
left_images = self._list_images(dst_raw_dir, "lc")
|
||||
right_images = self._list_images(dst_raw_dir, right_camera)
|
||||
|
||||
if not left_images or not right_images:
|
||||
stats["skipped"] += 1
|
||||
print(
|
||||
f"[WARN] {session_name}/{scan_name} {pair_name}: "
|
||||
f"missing images (lc={len(left_images)}, {right_camera}={len(right_images)})."
|
||||
)
|
||||
continue
|
||||
|
||||
pairs = self._pair_images(left_images, right_images, right_camera)
|
||||
if not pairs:
|
||||
stats["skipped"] += 1
|
||||
print(f"[WARN] {session_name}/{scan_name} {pair_name}: no valid pairs.")
|
||||
continue
|
||||
|
||||
save_lc_this_pair = (
|
||||
pair_name == self.keep_lc_from_pair
|
||||
or (not lc_written and pair_name != self.keep_lc_from_pair)
|
||||
)
|
||||
|
||||
stats["pairs_total"] += len(pairs)
|
||||
for left_path, right_path in tqdm(
|
||||
pairs,
|
||||
desc=f"{session_name}/{scan_name} {pair_name}",
|
||||
unit="pair",
|
||||
leave=False,
|
||||
):
|
||||
left_img = cv2.imread(str(left_path), cv2.IMREAD_COLOR)
|
||||
right_img = cv2.imread(str(right_path), cv2.IMREAD_COLOR)
|
||||
if left_img is None or right_img is None:
|
||||
stats["skipped"] += 1
|
||||
continue
|
||||
|
||||
left_rect, right_rect = self._rectify_pair_image(pair_name, left_img, right_img)
|
||||
if save_lc_this_pair:
|
||||
left_out = dst_rect_dir / left_path.name
|
||||
cv2.imwrite(str(left_out), left_rect)
|
||||
lc_written = True
|
||||
right_out = dst_rect_dir / right_path.name
|
||||
cv2.imwrite(str(right_out), right_rect)
|
||||
stats["saved"] += 1
|
||||
|
||||
return stats
|
||||
|
||||
def _discover_session_scan_raw_dirs(self) -> List[Tuple[str, str]]:
|
||||
found: List[Tuple[str, str]] = []
|
||||
session_dirs = sorted(
|
||||
[p for p in self.source_date_root.iterdir() if p.is_dir() and p.name.lower().startswith("session")]
|
||||
)
|
||||
for session_dir in session_dirs:
|
||||
if self.session_filter and session_dir.name != self.session_filter:
|
||||
continue
|
||||
scan_dirs = sorted(
|
||||
[p for p in session_dir.iterdir() if p.is_dir() and p.name.lower().startswith("scan")]
|
||||
)
|
||||
for scan_dir in scan_dirs:
|
||||
raw_dir = scan_dir / "01_raw_images"
|
||||
if raw_dir.is_dir():
|
||||
found.append((session_dir.name, scan_dir.name))
|
||||
return found
|
||||
|
||||
def run_batch(self) -> Dict[str, int]:
|
||||
all_scans = self._discover_session_scan_raw_dirs()
|
||||
if not all_scans:
|
||||
raise RuntimeError(f"No scan folders found under {self.source_date_root}")
|
||||
|
||||
print(f"[INFO] Found {len(all_scans)} scans under {self.source_date_root}")
|
||||
totals = {"scans": 0, "pairs_total": 0, "saved": 0, "skipped": 0}
|
||||
|
||||
sessions_seen = set()
|
||||
for session_name, scan_name in all_scans:
|
||||
if session_name not in sessions_seen:
|
||||
self._copy_params_link_for_session(session_name)
|
||||
sessions_seen.add(session_name)
|
||||
|
||||
scan_stats = self._process_scan(session_name, scan_name)
|
||||
totals["scans"] += 1
|
||||
totals["pairs_total"] += scan_stats["pairs_total"]
|
||||
totals["saved"] += scan_stats["saved"]
|
||||
totals["skipped"] += scan_stats["skipped"]
|
||||
|
||||
print(
|
||||
"[INFO] Batch rectification finished: "
|
||||
f"scans={totals['scans']} pairs={totals['pairs_total']} "
|
||||
f"saved={totals['saved']} skipped={totals['skipped']}"
|
||||
)
|
||||
return totals
|
||||
@@ -0,0 +1,7 @@
|
||||
# 04_Rectification — Python dependencies
|
||||
# Install: pip install -r requirements.txt
|
||||
# Full pipeline (all steps): pip install -r ~/Speckle-Scanner/requirements.txt
|
||||
|
||||
numpy>=1.21
|
||||
opencv-python>=4.8
|
||||
tqdm>=4.0
|
||||
Reference in New Issue
Block a user