ajustes na calibragem e visualizacao
This commit is contained in:
parent
1f871dc700
commit
6e166748cf
|
|
@ -175,19 +175,15 @@ class OakFcc3Client:
|
|||
return self.svc.capture_frame(timeout=timeout)
|
||||
|
||||
def get_next_frame(self, timeout=2.0):
|
||||
frame, meta, _ = self.get_next_decoded(
|
||||
timeout=timeout,
|
||||
update_radiometry=False,
|
||||
)
|
||||
frame, meta, _ = self.get_next_decoded(timeout=timeout)
|
||||
return frame, meta
|
||||
|
||||
def get_next_decoded(self, timeout=2.0, update_radiometry=True):
|
||||
def get_next_decoded(self, timeout=2.0):
|
||||
raw_frame, raw_meta = self.get_next_raw_frame(timeout=timeout)
|
||||
|
||||
decoded = self.decode_stream_cameras(raw_frame, raw_meta)
|
||||
|
||||
if update_radiometry:
|
||||
self.update_radiometry(decoded, raw_meta)
|
||||
self.update_radiometry(decoded, raw_meta)
|
||||
|
||||
frame_type = str(raw_meta.get("frame_type", self.frame_type)).upper()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class RawProcessorCore:
|
|||
self.sensor_width = sensor_width
|
||||
self.sensor_height = sensor_height
|
||||
self.bayer_pattern = bayer_pattern.upper()
|
||||
|
||||
self.fusion_config = {
|
||||
"alignment_mode": "manual_affine",
|
||||
"baseline_mm": 75.0,
|
||||
|
|
@ -26,6 +27,7 @@ class RawProcessorCore:
|
|||
"resize_after_crop": True,
|
||||
"target_size": None,
|
||||
}
|
||||
|
||||
self.rgb_calibration = {
|
||||
"enabled": False,
|
||||
"gains": {
|
||||
|
|
@ -53,6 +55,14 @@ class RawProcessorCore:
|
|||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
|
||||
self.radiometric_normalization_config = {
|
||||
"enabled": False,
|
||||
"method": "exposure_gain_reference",
|
||||
"reference_controls": {},
|
||||
"clip_output": False,
|
||||
}
|
||||
self.camera_settings = {}
|
||||
|
||||
if calibration_json_path:
|
||||
self.load_fusion_config_json(calibration_json_path)
|
||||
|
||||
|
|
@ -469,10 +479,67 @@ class RawProcessorCore:
|
|||
|
||||
return decoded
|
||||
|
||||
def _decode_spectral_frame_to_float01(self, data, cam_meta):
|
||||
arr = data
|
||||
|
||||
if arr.ndim == 3 and arr.shape[2] == 1:
|
||||
arr = arr[:, :, 0]
|
||||
|
||||
bit_depth = int(cam_meta.get("bit_depth", 8))
|
||||
raw_format = str(cam_meta.get("raw_format", "")).upper()
|
||||
packed = bool(cam_meta.get("packed", False))
|
||||
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
|
||||
|
||||
sensor_width = int(cam_meta.get("width", self.sensor_width))
|
||||
sensor_height = int(cam_meta.get("height", arr.shape[0]))
|
||||
packed_width = int(cam_meta.get("packed_width", 0) or 0)
|
||||
|
||||
looks_like_raw10_packed = (
|
||||
arr.ndim == 2
|
||||
and arr.dtype == np.uint8
|
||||
and arr.shape[0] == sensor_height
|
||||
and (
|
||||
raw_format == "RAW10_PACKED"
|
||||
or packed
|
||||
or bit_depth == 10
|
||||
or (packed_width > 0 and arr.shape[1] == packed_width and packed_width != sensor_width)
|
||||
or arr.shape[1] == int(sensor_width * 10 / 8)
|
||||
)
|
||||
)
|
||||
|
||||
if looks_like_raw10_packed:
|
||||
raw16 = self.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=sensor_width,
|
||||
sensor_height=sensor_height,
|
||||
)
|
||||
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
|
||||
# Caso preview/processado: mono já vem uint8 normal.
|
||||
if arr.ndim == 2 and arr.dtype == np.uint8:
|
||||
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
|
||||
|
||||
if arr.ndim == 2 and arr.dtype == np.uint16:
|
||||
max_val = float((1 << bit_depth) - 1) if bit_depth > 0 and bit_depth <= 16 else 65535.0
|
||||
return np.clip(arr.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
|
||||
arr01 = arr.astype(np.float32)
|
||||
if arr01.max() > 1.5:
|
||||
arr01 /= 255.0
|
||||
|
||||
return np.clip(arr01, 0.0, 1.0)
|
||||
|
||||
def fuse_multispec_cameras(self, decoded, meta, channels_expected):
|
||||
# Flat-field pertence ao espaço nativo de cada câmera.
|
||||
# Por isso é aplicado antes de warp/homografia/crop comum.
|
||||
decoded = self.apply_flatfield_to_decoded(decoded)
|
||||
# 1) Coloca todos os frames na mesma escala de exposição/ganho de referência
|
||||
decoded = self.normalize_decoded_by_capture_controls(decoded, meta)
|
||||
|
||||
# 2) Subtrai dark/offset no espaço individual de cada câmera
|
||||
decoded = self.apply_dark_to_decoded(decoded)
|
||||
|
||||
# 3) Aplica o ganho espacial do flat field no espaço individual de cada câmera
|
||||
decoded = self.apply_flat_gain_to_decoded(decoded)
|
||||
|
||||
rgb_cam_id = self._find_cam_by_role(decoded, "rgb")
|
||||
if rgb_cam_id is None:
|
||||
|
|
@ -801,7 +868,6 @@ class RawProcessorCore:
|
|||
|
||||
return arr.reshape(shape)
|
||||
|
||||
|
||||
def save_rgb_u8_file(self, path: str, arr: np.ndarray):
|
||||
arr.astype(np.uint8).tofile(path)
|
||||
|
||||
|
|
@ -862,23 +928,47 @@ class RawProcessorCore:
|
|||
|
||||
fusion = data.get("fusion_config")
|
||||
if isinstance(fusion, dict):
|
||||
self.fusion_config = self._merge_fusion_config(self.fusion_config, fusion)
|
||||
self.fusion_config = self._merge_config(self.fusion_config, fusion)
|
||||
else:
|
||||
print("[WARN] JSON sem fusion_config. Mantendo config padrão.")
|
||||
|
||||
rgb_cal = data.get("rgb_calibration")
|
||||
if isinstance(rgb_cal, dict):
|
||||
self.rgb_calibration = self._merge_fusion_config(self.rgb_calibration, rgb_cal)
|
||||
self.rgb_calibration = self._merge_config(self.rgb_calibration, rgb_cal)
|
||||
|
||||
flatfield = data.get("flatfield_config")
|
||||
if isinstance(flatfield, dict):
|
||||
self.flatfield_config = self._merge_fusion_config(self.flatfield_config, flatfield)
|
||||
self.flatfield_config = self._merge_config(self.flatfield_config, flatfield)
|
||||
self.load_flatfield_maps()
|
||||
else:
|
||||
self.flatfield_config["enabled"] = False
|
||||
self.flatfield_maps = {}
|
||||
self.flatfield_loaded = False
|
||||
|
||||
rad_norm_config = data.get("radiometric_normalization")
|
||||
if isinstance(rad_norm_config, dict):
|
||||
self.radiometric_normalization_config = self._merge_config(self.radiometric_normalization_config, rad_norm_config)
|
||||
|
||||
cam_set = data.get("camera_settings")
|
||||
if isinstance(cam_set, dict):
|
||||
self.camera_settings = self._merge_config(self.camera_settings, cam_set)
|
||||
|
||||
def _merge_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
|
||||
cfg = json.loads(json.dumps(default_cfg))
|
||||
|
||||
def merge(dst: dict, src: dict):
|
||||
for key, value in src.items():
|
||||
if isinstance(value, dict) and isinstance(dst.get(key), dict):
|
||||
merge(dst[key], value)
|
||||
else:
|
||||
dst[key] = value
|
||||
|
||||
if isinstance(loaded_cfg, dict):
|
||||
merge(cfg, loaded_cfg)
|
||||
|
||||
return cfg
|
||||
|
||||
|
||||
def _resolve_calibration_path(self, path: str) -> str:
|
||||
if not path:
|
||||
return ""
|
||||
|
|
@ -966,12 +1056,16 @@ class RawProcessorCore:
|
|||
|
||||
return self.flatfield_loaded
|
||||
|
||||
def apply_flatfield_to_decoded(self, decoded: dict) -> dict:
|
||||
def apply_dark_to_decoded(self, decoded: dict) -> dict:
|
||||
cfg = self.flatfield_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
subtract_dark = bool(cfg.get("subtract_dark", True))
|
||||
if not subtract_dark:
|
||||
return decoded
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
self.load_flatfield_maps()
|
||||
|
||||
|
|
@ -990,8 +1084,6 @@ class RawProcessorCore:
|
|||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
subtract_dark = bool(cfg.get("subtract_dark", False))
|
||||
clip_output = bool(cfg.get("clip_output", True))
|
||||
|
||||
if role == "rgb":
|
||||
if img.ndim != 3 or img.shape[2] < 3:
|
||||
|
|
@ -999,11 +1091,70 @@ class RawProcessorCore:
|
|||
continue
|
||||
|
||||
out = img.astype(np.float32).copy()
|
||||
|
||||
for idx, ch in enumerate(("R", "G", "B")):
|
||||
out[:, :, idx] = self._apply_flatfield_single_channel(
|
||||
out[:, :, idx] = self._subtract_dark_single_channel(
|
||||
out[:, :, idx],
|
||||
ch,
|
||||
)
|
||||
|
||||
new_item["image"] = out
|
||||
|
||||
elif role in ("re", "nir"):
|
||||
ch = "RE" if role == "re" else "NIR"
|
||||
|
||||
new_item["image"] = self._subtract_dark_single_channel(
|
||||
img.astype(np.float32),
|
||||
ch,
|
||||
)
|
||||
|
||||
else:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_meta["dark_applied"] = True
|
||||
new_item["meta"] = new_meta
|
||||
corrected[cam_id] = new_item
|
||||
|
||||
return corrected
|
||||
|
||||
def apply_flat_gain_to_decoded(self, decoded: dict) -> dict:
|
||||
cfg = self.flatfield_config or {}
|
||||
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
self.load_flatfield_maps()
|
||||
|
||||
if not self.flatfield_loaded:
|
||||
return decoded
|
||||
|
||||
clip_output = bool(cfg.get("clip_output", True))
|
||||
corrected = {}
|
||||
|
||||
for cam_id, item in decoded.items():
|
||||
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||
img = item.get("image")
|
||||
|
||||
if img is None:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
|
||||
if role == "rgb":
|
||||
if img.ndim != 3 or img.shape[2] < 3:
|
||||
corrected[cam_id] = item
|
||||
continue
|
||||
|
||||
out = img.astype(np.float32).copy()
|
||||
|
||||
for idx, ch in enumerate(("R", "G", "B")):
|
||||
out[:, :, idx] = self._apply_flat_gain_single_channel(
|
||||
out[:, :, idx],
|
||||
ch,
|
||||
subtract_dark=subtract_dark,
|
||||
clip_output=clip_output,
|
||||
)
|
||||
|
||||
|
|
@ -1011,10 +1162,10 @@ class RawProcessorCore:
|
|||
|
||||
elif role in ("re", "nir"):
|
||||
ch = "RE" if role == "re" else "NIR"
|
||||
new_item["image"] = self._apply_flatfield_single_channel(
|
||||
|
||||
new_item["image"] = self._apply_flat_gain_single_channel(
|
||||
img.astype(np.float32),
|
||||
ch,
|
||||
subtract_dark=subtract_dark,
|
||||
clip_output=clip_output,
|
||||
)
|
||||
|
||||
|
|
@ -1029,11 +1180,38 @@ class RawProcessorCore:
|
|||
|
||||
return corrected
|
||||
|
||||
def _apply_flatfield_single_channel(
|
||||
def _subtract_dark_single_channel(
|
||||
self,
|
||||
img: np.ndarray,
|
||||
channel_name: str,
|
||||
) -> np.ndarray:
|
||||
ch = str(channel_name).upper()
|
||||
entry = self.flatfield_maps.get(ch)
|
||||
|
||||
if not entry:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
dark = entry.get("dark")
|
||||
if dark is None:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
base = img.astype(np.float32)
|
||||
|
||||
dark = dark.astype(np.float32)
|
||||
if dark.shape[:2] != base.shape[:2]:
|
||||
dark = cv2.resize(
|
||||
dark,
|
||||
(base.shape[1], base.shape[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
out = np.maximum(base - dark, 0.0)
|
||||
return out.astype(np.float32, copy=False)
|
||||
|
||||
def _apply_flat_gain_single_channel(
|
||||
self,
|
||||
img: np.ndarray,
|
||||
channel_name: str,
|
||||
subtract_dark: bool = False,
|
||||
clip_output: bool = True,
|
||||
) -> np.ndarray:
|
||||
ch = str(channel_name).upper()
|
||||
|
|
@ -1046,71 +1224,133 @@ class RawProcessorCore:
|
|||
if gain is None:
|
||||
return img.astype(np.float32, copy=False)
|
||||
|
||||
if gain.shape[:2] != img.shape[:2]:
|
||||
base = img.astype(np.float32)
|
||||
|
||||
gain = gain.astype(np.float32)
|
||||
if gain.shape[:2] != base.shape[:2]:
|
||||
gain = cv2.resize(
|
||||
gain.astype(np.float32),
|
||||
(img.shape[1], img.shape[0]),
|
||||
gain,
|
||||
(base.shape[1], base.shape[0]),
|
||||
interpolation=cv2.INTER_LINEAR,
|
||||
)
|
||||
|
||||
base = img.astype(np.float32)
|
||||
|
||||
if subtract_dark and "dark" in entry:
|
||||
dark = entry["dark"].astype(np.float32)
|
||||
if dark.shape[:2] != img.shape[:2]:
|
||||
dark = cv2.resize(dark, (img.shape[1], img.shape[0]), interpolation=cv2.INTER_LINEAR)
|
||||
base = np.maximum(base - dark, 0.0)
|
||||
|
||||
out = base * gain.astype(np.float32)
|
||||
out = base * gain
|
||||
|
||||
if clip_output:
|
||||
out = np.clip(out, 0.0, 1.0)
|
||||
|
||||
return out.astype(np.float32, copy=False)
|
||||
|
||||
def _merge_fusion_config(self, default_cfg: dict, loaded_cfg: dict) -> dict:
|
||||
cfg = json.loads(json.dumps(default_cfg))
|
||||
|
||||
for key, value in loaded_cfg.items():
|
||||
if isinstance(value, dict) and isinstance(cfg.get(key), dict):
|
||||
cfg[key].update(value)
|
||||
else:
|
||||
cfg[key] = value
|
||||
def normalize_decoded_by_capture_controls(self, decoded: dict, meta: dict | None = None) -> dict:
|
||||
cfg = self.radiometric_normalization_config or {}
|
||||
|
||||
return cfg
|
||||
if not cfg.get("enabled", False):
|
||||
return decoded
|
||||
|
||||
def _decode_spectral_frame_to_float01(self, data, cam_meta):
|
||||
arr = data
|
||||
method = str(cfg.get("method", "exposure_gain_reference")).lower()
|
||||
if method != "exposure_gain_reference":
|
||||
return decoded
|
||||
|
||||
if arr.ndim == 3 and arr.shape[2] == 1:
|
||||
arr = arr[:, :, 0]
|
||||
controls = self._extract_actual_controls_from_meta(meta)
|
||||
if not controls:
|
||||
return decoded
|
||||
|
||||
bit_depth = int(cam_meta.get("bit_depth", 8))
|
||||
channels = int(cam_meta.get("channels", 1)) if cam_meta.get("channels") is not None else 1
|
||||
reference_controls = cfg.get("reference_controls", {}) or {}
|
||||
clip_output = bool(cfg.get("clip_output", False))
|
||||
|
||||
# Caso preview/processado: mono já vem uint8/uint16 normal.
|
||||
if arr.ndim == 2 and arr.dtype == np.uint8:
|
||||
return np.clip(arr.astype(np.float32) / 255.0, 0.0, 1.0)
|
||||
normalized = {}
|
||||
|
||||
if arr.ndim == 2 and arr.dtype == np.uint16 and bit_depth != 10:
|
||||
return np.clip(arr.astype(np.float32) / 65535.0, 0.0, 1.0)
|
||||
for cam_id, item in decoded.items():
|
||||
role = str(item.get("role") or item.get("meta", {}).get("role") or "").lower()
|
||||
img = item.get("image")
|
||||
|
||||
if bit_depth == 10:
|
||||
sensor_width = int(cam_meta.get("width", self.sensor_width))
|
||||
sensor_height = int(cam_meta.get("height", arr.shape[0]))
|
||||
if img is None or not role:
|
||||
normalized[cam_id] = item
|
||||
continue
|
||||
|
||||
raw16 = self.unpack_raw10_packed(
|
||||
arr,
|
||||
sensor_width=sensor_width,
|
||||
sensor_height=sensor_height,
|
||||
actual_ctrl = controls.get(role, {}) or {}
|
||||
|
||||
ref_ctrl = (
|
||||
reference_controls.get(role)
|
||||
or self.camera_settings.get(role)
|
||||
or actual_ctrl
|
||||
or {}
|
||||
)
|
||||
|
||||
max_val = float((1 << bit_depth) - 1)
|
||||
return np.clip(raw16.astype(np.float32) / max_val, 0.0, 1.0)
|
||||
actual_factor = self._exposure_gain_factor(actual_ctrl)
|
||||
ref_factor = self._exposure_gain_factor(ref_ctrl)
|
||||
|
||||
# fallback
|
||||
arr01 = arr.astype(np.float32)
|
||||
if arr01.max() > 1.5:
|
||||
arr01 /= 255.0
|
||||
if actual_factor <= 0 or ref_factor <= 0:
|
||||
normalized[cam_id] = item
|
||||
continue
|
||||
|
||||
return np.clip(arr01, 0.0, 1.0)
|
||||
scale = ref_factor / actual_factor
|
||||
|
||||
new_item = dict(item)
|
||||
new_meta = dict(item.get("meta", {}) or {})
|
||||
|
||||
out = img.astype(np.float32) * float(scale)
|
||||
|
||||
if clip_output:
|
||||
out = np.clip(out, 0.0, 1.0)
|
||||
|
||||
new_meta["radiometric_normalization_applied"] = True
|
||||
new_meta["radiometric_normalization_method"] = method
|
||||
new_meta["radiometric_normalization_scale"] = float(scale)
|
||||
new_meta["radiometric_actual_factor"] = float(actual_factor)
|
||||
new_meta["radiometric_reference_factor"] = float(ref_factor)
|
||||
|
||||
new_item["image"] = out.astype(np.float32, copy=False)
|
||||
new_item["meta"] = new_meta
|
||||
|
||||
normalized[cam_id] = new_item
|
||||
|
||||
return normalized
|
||||
|
||||
def _extract_actual_controls_from_meta(self, meta: dict | None) -> dict:
|
||||
if not meta:
|
||||
return {}
|
||||
|
||||
# Preferência: controles reais daquele frame.
|
||||
controls = meta.get("actual_camera_controls")
|
||||
if isinstance(controls, dict) and controls:
|
||||
return controls
|
||||
|
||||
# Possíveis nomes alternativos.
|
||||
controls = meta.get("camera_controls")
|
||||
if isinstance(controls, dict) and controls:
|
||||
return controls
|
||||
|
||||
controls = meta.get("startup_camera_controls")
|
||||
if isinstance(controls, dict) and controls:
|
||||
return controls
|
||||
|
||||
# Em alguns casos o JSON da captura pode ter stream_meta separado,
|
||||
# mas se o meta recebido aqui for só stream_meta, talvez não tenha controles.
|
||||
return {}
|
||||
|
||||
def _exposure_gain_factor(self, ctrl: dict) -> float:
|
||||
if not isinstance(ctrl, dict):
|
||||
return 0.0
|
||||
|
||||
exp = ctrl.get("exposure_time_us", None)
|
||||
gain = ctrl.get("analogue_gain", None)
|
||||
|
||||
try:
|
||||
exp = float(exp)
|
||||
except Exception:
|
||||
exp = 0.0
|
||||
|
||||
try:
|
||||
gain = float(gain)
|
||||
except Exception:
|
||||
gain = 1.0
|
||||
|
||||
if exp <= 0:
|
||||
return 0.0
|
||||
|
||||
if gain <= 0:
|
||||
gain = 1.0
|
||||
|
||||
return float(exp * gain)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,112 @@ def chw_to_hwc(arr: np.ndarray) -> np.ndarray:
|
|||
return np.transpose(arr, (1, 2, 0))
|
||||
|
||||
|
||||
def tensor_to_preview_panels(tensor: np.ndarray):
|
||||
"""
|
||||
Recebe tensor CHW [R,G,B,RE,NIR] float32 e devolve painéis visuais.
|
||||
"""
|
||||
if tensor.ndim != 3 or tensor.shape[0] < 5:
|
||||
raise RuntimeError(f"Tensor MULTISPEC inválido: shape={tensor.shape}")
|
||||
|
||||
rgb_hwc = np.transpose(tensor[:3].astype(np.float32), (1, 2, 0))
|
||||
rgb_bgr = normalize_float01_to_bgr(rgb_hwc)
|
||||
|
||||
re01 = tensor[3].astype(np.float32)
|
||||
nir01 = tensor[4].astype(np.float32)
|
||||
|
||||
re_bgr = cv2.cvtColor(
|
||||
np.clip(re01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
nir_bgr = cv2.cvtColor(
|
||||
np.clip(nir01 * 255.0, 0, 255).astype(np.uint8),
|
||||
cv2.COLOR_GRAY2BGR
|
||||
)
|
||||
|
||||
return [
|
||||
("MULTISPEC RGB final", rgb_bgr, f"tensor {list(tensor.shape)} | canais 0,1,2"),
|
||||
("MULTISPEC RE final", re_bgr, "tensor canal 3"),
|
||||
("MULTISPEC NIR final", nir_bgr, "tensor canal 4"),
|
||||
]
|
||||
|
||||
|
||||
def build_multispec_from_raw_native_multi(group: dict, meta: dict):
|
||||
"""
|
||||
Reconstrói o tensor MULTISPEC final a partir dos .bin RAW_BRUTO salvos.
|
||||
|
||||
Usa:
|
||||
- saved_payload_paths
|
||||
- saved_payload_shapes
|
||||
- saved_payload_dtypes
|
||||
- stream_meta.camera_info
|
||||
- camera_params_json/module_params.json
|
||||
"""
|
||||
if meta.get("saved_payload_type") != "raw_native_multi":
|
||||
return None, "captura não é raw_native_multi"
|
||||
|
||||
stream_meta = meta.get("stream_meta", {}) or {}
|
||||
camera_info = stream_meta.get("camera_info", {}) or {}
|
||||
|
||||
saved_dtypes = meta.get("saved_payload_dtypes", {}) or {}
|
||||
saved_shapes = meta.get("saved_payload_shapes", {}) or {}
|
||||
|
||||
frame = {}
|
||||
|
||||
for cam_id, path in group["cameras"].items():
|
||||
saved_dtype = saved_dtypes.get(cam_id)
|
||||
saved_shape = saved_shapes.get(cam_id)
|
||||
|
||||
if saved_dtype is None or saved_shape is None:
|
||||
raise RuntimeError(f"Faltam dtype/shape para {cam_id}")
|
||||
|
||||
arr = np.fromfile(str(path), dtype=np.dtype(saved_dtype)).reshape(tuple(saved_shape))
|
||||
frame[cam_id] = arr
|
||||
|
||||
if not frame:
|
||||
raise RuntimeError("Nenhum payload de câmera encontrado para reconstruir MULTISPEC.")
|
||||
|
||||
sensor_width = int(meta.get("sensor_width", 1280))
|
||||
sensor_height = int(meta.get("sensor_height", 800))
|
||||
bayer = meta.get("bayer_pattern", "RGGB")
|
||||
|
||||
# Tenta usar o mesmo module_params que foi usado na captura.
|
||||
calib_path = meta.get("camera_params_json") or "calibration/module_params.json"
|
||||
|
||||
# Se vier relativo, tenta resolver relativo ao diretório atual.
|
||||
# Normalmente seu script roda da raiz do projeto, então calibration/module_params.json funciona.
|
||||
if calib_path and not os.path.isfile(calib_path):
|
||||
# fallback: tenta relativo à pasta do JSON
|
||||
json_dir = Path(group["json"]).parent
|
||||
alt = json_dir / calib_path
|
||||
if alt.exists():
|
||||
calib_path = str(alt)
|
||||
else:
|
||||
print(f"[WARN] module_params não encontrado: {calib_path}. Tentando sem calibração.")
|
||||
calib_path = None
|
||||
|
||||
core = RawProcessorCore(
|
||||
sensor_width=sensor_width,
|
||||
sensor_height=sensor_height,
|
||||
bayer_pattern=bayer,
|
||||
calibration_json_path=calib_path,
|
||||
)
|
||||
|
||||
# O decode precisa do stream_meta com camera_info.
|
||||
processing_meta = dict(stream_meta)
|
||||
|
||||
# A normalização radiométrica precisa dos controles reais salvos no JSON da captura.
|
||||
if meta.get("actual_camera_controls") is not None:
|
||||
processing_meta["actual_camera_controls"] = meta.get("actual_camera_controls")
|
||||
|
||||
if meta.get("startup_camera_controls") is not None:
|
||||
processing_meta["startup_camera_controls"] = meta.get("startup_camera_controls")
|
||||
|
||||
tensor = core.build_infer_tensor_from_stream(frame, processing_meta, 5)
|
||||
|
||||
return tensor, f"MULTISPEC gerado offline do RAW_BRUTO | shape={list(tensor.shape)}"
|
||||
|
||||
|
||||
def build_visual_from_saved_payload(payload_path: Path, meta: dict, cam_id: str | None = None) -> tuple[np.ndarray, str]:
|
||||
"""
|
||||
Retorna:
|
||||
|
|
@ -236,12 +342,51 @@ def build_panels_from_group(group):
|
|||
panels = []
|
||||
|
||||
meta = load_json(group["json"])
|
||||
saved_type = meta.get("saved_payload_type")
|
||||
|
||||
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
|
||||
if preview_saved is None:
|
||||
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
|
||||
panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}"))
|
||||
# =========================================================
|
||||
# Para RAW_BRUTO multi, o primeiro painel vira o tensor final
|
||||
# gerado offline a partir dos .bin salvos.
|
||||
# =========================================================
|
||||
if saved_type == "raw_native_multi":
|
||||
try:
|
||||
tensor, desc = build_multispec_from_raw_native_multi(group, meta)
|
||||
tensor_panels = tensor_to_preview_panels(tensor)
|
||||
|
||||
# Aqui colocamos só o RGB final como painel principal,
|
||||
# para substituir o antigo PNG salvo.
|
||||
title, img, subtitle = tensor_panels[0]
|
||||
panels.append((title, img, desc))
|
||||
|
||||
# Opcional: se quiser também ver RE/NIR finais do tensor,
|
||||
# descomente estas duas linhas:
|
||||
# panels.append(tensor_panels[1])
|
||||
# panels.append(tensor_panels[2])
|
||||
|
||||
except Exception as e:
|
||||
# Fallback para o PNG salvo caso a reconstrução falhe.
|
||||
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
|
||||
if preview_saved is None:
|
||||
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
|
||||
|
||||
panels.append((
|
||||
"Preview salvo fallback",
|
||||
preview_saved,
|
||||
f"Falha ao gerar MULTISPEC offline: {e}"
|
||||
))
|
||||
|
||||
else:
|
||||
# Para RGB/MULTISPEC salvos direto, mantém comportamento antigo.
|
||||
preview_saved = cv2.imread(str(group["png"]), cv2.IMREAD_COLOR)
|
||||
if preview_saved is None:
|
||||
raise RuntimeError(f"Falha ao ler preview PNG: {group['png']}")
|
||||
|
||||
panels.append(("Preview salvo", preview_saved, f"{preview_saved.shape[1]}x{preview_saved.shape[0]}"))
|
||||
|
||||
# =========================================================
|
||||
# Se houver payload final único, reconstrói normalmente.
|
||||
# Ex: saved_payload_type == multispec
|
||||
# =========================================================
|
||||
if group["final_raw"] is not None:
|
||||
result, desc = build_visual_from_saved_payload(group["final_raw"], meta)
|
||||
|
||||
|
|
@ -251,6 +396,9 @@ def build_panels_from_group(group):
|
|||
else:
|
||||
panels.append(("Reconstruido (final)", result, desc))
|
||||
|
||||
# =========================================================
|
||||
# Continua mostrando CAM_A/CAM_B/CAM_C reconstruídas individualmente.
|
||||
# =========================================================
|
||||
for cam_id, path in group["cameras"].items():
|
||||
img, desc = build_visual_from_saved_payload(path, meta, cam_id=cam_id)
|
||||
panels.append((f"{cam_id} reconstruido", img, desc))
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ def get_image_by_role(decoded: dict, role: str):
|
|||
|
||||
def validate_module_ready(status: dict, raw_policy: str):
|
||||
if not status.get("ok", True):
|
||||
raise RuntimeError(f"Status inválido retornado pelo módulo: {status}")
|
||||
raise RuntimeError(f"Status invalido retornado pelo modulo: {status}")
|
||||
|
||||
active_roles = status.get("active_roles", {}) or {}
|
||||
active_count = int(status.get("camera_count_active", 0))
|
||||
|
|
@ -254,6 +254,19 @@ def extract_channels_from_decoded(decoded: dict) -> dict:
|
|||
_, re01 = get_image_by_role(decoded, "re")
|
||||
_, nir01 = get_image_by_role(decoded, "nir")
|
||||
|
||||
def assert_not_raw10_packed_image(role, img, expected_w=1280):
|
||||
if img is None:
|
||||
return
|
||||
|
||||
if img.ndim == 2 and img.shape[1] == int(expected_w * 10 / 8):
|
||||
raise RuntimeError(
|
||||
f"{role.upper()} parece RAW10_PACKED interpretado como imagem: "
|
||||
f"shape={img.shape}. Esperado decodificado com largura {expected_w}."
|
||||
)
|
||||
|
||||
assert_not_raw10_packed_image("re", re01, expected_w=1280)
|
||||
assert_not_raw10_packed_image("nir", nir01, expected_w=1280)
|
||||
|
||||
out = {}
|
||||
|
||||
if rgb01 is not None:
|
||||
|
|
@ -354,7 +367,7 @@ def build_board(decoded, controls, state_lines, progress_lines, preview_scale=1.
|
|||
"S = pular etapa dark/preto",
|
||||
"Q / ESC = sair sem salvar",
|
||||
"",
|
||||
"Dica: branco/preto devem preencher todo o campo de visão.",
|
||||
"Dica: branco/preto devem preencher todo o campo de visao.",
|
||||
"Para dark-frame perfeito, tampe as lentes em vez de usar fundo preto.",
|
||||
])
|
||||
|
||||
|
|
@ -420,12 +433,12 @@ def capture_stage(
|
|||
progress_lines = [
|
||||
f"Etapa: {stage_name}",
|
||||
f"Descartando frames iniciais: {len(seen_frame_ids)}/{discard_frames}",
|
||||
"Aguardando estabilização de exposição/stream...",
|
||||
"Aguardando estabilizacao de exposicao/stream...",
|
||||
]
|
||||
board = build_board(
|
||||
decoded=last_decoded,
|
||||
controls=last_controls,
|
||||
state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"],
|
||||
state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"],
|
||||
progress_lines=progress_lines,
|
||||
preview_scale=preview_scale,
|
||||
)
|
||||
|
|
@ -458,7 +471,7 @@ def capture_stage(
|
|||
board = build_board(
|
||||
decoded=last_decoded,
|
||||
controls=last_controls,
|
||||
state_lines=[f"CALIBRAÇÃO FLAT-FIELD - {stage_name.upper()}"],
|
||||
state_lines=[f"CALIBRACAO FLAT-FIELD - {stage_name.upper()}"],
|
||||
progress_lines=progress_lines,
|
||||
preview_scale=preview_scale,
|
||||
)
|
||||
|
|
@ -473,7 +486,7 @@ def capture_stage(
|
|||
|
||||
k = cv2.waitKey(1) & 0xFF
|
||||
if k in (ord("q"), ord("Q"), 27):
|
||||
raise KeyboardInterrupt("Captura cancelada pelo usuário.")
|
||||
raise KeyboardInterrupt("Captura cancelada pelo usuario.")
|
||||
|
||||
return channel_stack, controls_log, meta_log
|
||||
|
||||
|
|
@ -579,10 +592,10 @@ def show_final_preview(window_name: str, gain_maps: dict, preview_scale: float):
|
|||
"Flat-field salvo com sucesso.",
|
||||
"ENTER/qualquer tecla = fechar",
|
||||
"",
|
||||
"Use estes mapas antes da fusão geométrica.",
|
||||
"Use estes mapas antes da fusao geometrica.",
|
||||
"",
|
||||
"Obs: RGB e mono podem ter shapes diferentes;",
|
||||
"isso é normal se o decode gerar resoluções distintas.",
|
||||
"isso e normal se o decode gerar resolucoes distintas.",
|
||||
], x=18, y=36)
|
||||
|
||||
top = np.hstack([panels[0], panels[1], panels[2]])
|
||||
|
|
@ -635,7 +648,7 @@ def wait_for_enter_or_skip(
|
|||
if allow_skip and k in (ord("s"), ord("S")):
|
||||
return "skip"
|
||||
if k in (ord("q"), ord("Q"), 27):
|
||||
raise KeyboardInterrupt("Cancelado pelo usuário.")
|
||||
raise KeyboardInterrupt("Cancelado pelo usuario.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -644,7 +657,7 @@ def wait_for_enter_or_skip(
|
|||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Calibrador automático de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.",
|
||||
description="Calibrador automatico de flat-field/dark-frame para o módulo RGB/RE/NIR OAK-FCC-3.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
|
||||
|
|
@ -721,7 +734,7 @@ def main():
|
|||
output_dtype="uint8",
|
||||
capture_mode=args.capture_mode,
|
||||
raw_policy=args.raw_policy,
|
||||
module_calibration_json=args.module_calibration_json
|
||||
module_calibration_json=args.module_calibration_json or None
|
||||
) as cam:
|
||||
|
||||
validate_module_ready(cam.get_status(), args.raw_policy)
|
||||
|
|
@ -731,11 +744,11 @@ def main():
|
|||
window_name=window_name,
|
||||
title="ETAPA 1/2 - WHITE / FLAT FIELD",
|
||||
instruction_lines=[
|
||||
"Posicione o módulo na altura real de operação.",
|
||||
"Aponte para uma superfície branca/cinza fosca, uniforme e sem textura.",
|
||||
"Evite reflexos, sombras laterais e saturação.",
|
||||
"A superfície deve preencher todo o campo de visão.",
|
||||
"Pressione ENTER para começar a captura WHITE.",
|
||||
"Posicione o modulo na altura real de operacao.",
|
||||
"Aponte para uma superficie branca/cinza fosca, uniforme e sem textura.",
|
||||
"Evite reflexos, sombras laterais e saturacao.",
|
||||
"A superficie deve preencher todo o campo de visao.",
|
||||
"Pressione ENTER para comecar a captura WHITE.",
|
||||
],
|
||||
preview_scale=args.preview_scale,
|
||||
allow_skip=False,
|
||||
|
|
@ -767,10 +780,10 @@ def main():
|
|||
window_name=window_name,
|
||||
title="ETAPA 2/2 - DARK / PRETO",
|
||||
instruction_lines=[
|
||||
"Agora faça a captura dark/preto.",
|
||||
"Melhor opção: tampe as lentes completamente.",
|
||||
"Agora faca a captura dark/preto.",
|
||||
"Melhor opcao: tampe as lentes completamente.",
|
||||
"Alternativa: use fundo preto fosco preenchendo todo o frame.",
|
||||
"Mantenha exposição/ganho iguais aos da etapa anterior, se possível.",
|
||||
"Mantenha exposicao/ganho iguais aos da etapa anterior, se possivel.",
|
||||
"Pressione ENTER para capturar DARK/PRETO.",
|
||||
],
|
||||
preview_scale=args.preview_scale,
|
||||
|
|
@ -801,7 +814,7 @@ def main():
|
|||
# Processamento robusto.
|
||||
processing_panel = np.zeros((720, 1280, 3), dtype=np.uint8)
|
||||
overlay_hud(processing_panel, [
|
||||
"Processando calibração flat-field...",
|
||||
"Processando calibracao flat-field...",
|
||||
"Calculando medianas robustas por canal.",
|
||||
"Gerando mapas de ganho e previews.",
|
||||
], x=40, y=80, font_scale=0.8, line_step=34)
|
||||
|
|
|
|||
|
|
@ -703,7 +703,7 @@ def main():
|
|||
validate_module_ready(cam.get_status(), args.raw_policy)
|
||||
|
||||
while True:
|
||||
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0, update_radiometry=False)
|
||||
raw_frame, raw_meta, decoded = cam.get_next_decoded(timeout=2.0)
|
||||
|
||||
if raw_meta is not None and raw_meta.get("frame_id") != last_frame_id:
|
||||
last_frame_id = raw_meta.get("frame_id")
|
||||
|
|
|
|||
Loading…
Reference in New Issue