clean up folder
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _candidate_config_dirs() -> list[Path]:
|
||||
"""
|
||||
Candidate config directories for editable installs and packaged wheels.
|
||||
"""
|
||||
here = Path(__file__).resolve().parent
|
||||
candidates = [
|
||||
here / "configs", # packaged wheel layout: orisys/configs/*.json
|
||||
here.parent.parent / "config", # editable install from repo: repo_root/config/*.json
|
||||
here.parent / "config", # fallback
|
||||
Path.cwd() / "config", # script launched from repo root
|
||||
]
|
||||
|
||||
unique: list[Path] = []
|
||||
for path in candidates:
|
||||
if path not in unique:
|
||||
unique.append(path)
|
||||
return unique
|
||||
|
||||
|
||||
def _get_config_dir() -> Path:
|
||||
for candidate in _candidate_config_dirs():
|
||||
if candidate.exists() and any(candidate.glob("*.json")):
|
||||
return candidate
|
||||
# Default to the first candidate so error messages are stable.
|
||||
return _candidate_config_dirs()[0]
|
||||
|
||||
|
||||
CONFIG_DIR = _get_config_dir()
|
||||
|
||||
|
||||
def _config_files() -> list[Path]:
|
||||
if not CONFIG_DIR.exists():
|
||||
return []
|
||||
return sorted(CONFIG_DIR.glob("*.json"))
|
||||
|
||||
|
||||
def _load_json(path: Path) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def load_config(config_name_or_path):
|
||||
"""
|
||||
Load sensor configuration from a JSON file path or config name.
|
||||
|
||||
Config lookup order:
|
||||
1. Exact file path (absolute or relative)
|
||||
2. File stem under the config directory (e.g. ``ddjx01`` -> ``config/ddjx01.json``)
|
||||
3. JSON field ``"name"`` matching the requested name (keeps ``prototype`` compatible)
|
||||
"""
|
||||
config_path = Path(config_name_or_path)
|
||||
|
||||
if config_path.exists() and config_path.is_file():
|
||||
if config_path.suffix.lower() != ".json":
|
||||
raise ValueError(f"Config file must be a JSON file: {config_path}")
|
||||
return _load_json(config_path)
|
||||
|
||||
if config_path.is_absolute():
|
||||
raise FileNotFoundError(f"Config file not found at absolute path: {config_path}")
|
||||
|
||||
direct = CONFIG_DIR / f"{config_name_or_path}.json"
|
||||
if direct.exists():
|
||||
return _load_json(direct)
|
||||
|
||||
# Backward-compatible alias resolution: support old names via JSON "name" field.
|
||||
for candidate in _config_files():
|
||||
try:
|
||||
data = _load_json(candidate)
|
||||
except Exception:
|
||||
continue
|
||||
if data.get("name") == config_name_or_path:
|
||||
return data
|
||||
|
||||
available = [f.stem for f in _config_files()]
|
||||
available_str = ", ".join(available) if available else "none"
|
||||
raise FileNotFoundError(
|
||||
f"Input config file not found with name or path: {config_name_or_path}\n"
|
||||
f" Tried as file path: {config_path} (not found)\n"
|
||||
f" Tried as config file: {direct} (not found)\n"
|
||||
f" Available config files: {available_str}"
|
||||
)
|
||||
|
||||
|
||||
def list_config():
|
||||
"""
|
||||
List all available JSON configuration files by file stem.
|
||||
"""
|
||||
return [f.stem for f in _config_files()]
|
||||
|
||||
|
||||
def export_config(config_name=None, path="./config/exported_config.json"):
|
||||
"""
|
||||
Export a configuration to a JSON file with readable formatting.
|
||||
"""
|
||||
config = load_config(config_name)
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
formatted = _format_json_value(config)
|
||||
f.write(formatted)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Config name: {config_name} exported to {path}")
|
||||
return config
|
||||
|
||||
|
||||
def _format_json_value(value, indent_level=0, indent_str=" "):
|
||||
if isinstance(value, dict):
|
||||
if not value:
|
||||
return "{}"
|
||||
items = []
|
||||
for k, v in value.items():
|
||||
key_str = json.dumps(k, ensure_ascii=False)
|
||||
value_str = _format_json_value(v, indent_level + 1, indent_str)
|
||||
items.append(f"{indent_str * (indent_level + 1)}{key_str}: {value_str}")
|
||||
return "{\n" + ",\n".join(items) + "\n" + indent_str * indent_level + "}"
|
||||
if isinstance(value, list):
|
||||
if not value:
|
||||
return "[]"
|
||||
items = [json.dumps(item, ensure_ascii=False) for item in value]
|
||||
return "[" + ", ".join(items) + "]"
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
@@ -1,172 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
def _get_config_dir():
|
||||
"""Resolve config directory, robustly handling Nuitka compiled paths."""
|
||||
|
||||
# 1. Start with the directory of this file
|
||||
current_path = Path(__file__).parent
|
||||
|
||||
# 2. Check strict relative location (standard case)
|
||||
# If we are in src/orisys/configs/__init__.py, configs is current_path
|
||||
if (current_path / "prototype.json").exists():
|
||||
return current_path
|
||||
|
||||
# 3. Walk up the directory tree to find a 'configs' folder containing 'prototype.json'
|
||||
# This handles nested virtual paths like .../orisys/orisys/configs
|
||||
# We look for a physical directory that matches our expectation
|
||||
|
||||
search_path = current_path
|
||||
# Limit depth to avoid infinite loops, though unlikely with .parent
|
||||
for _ in range(5):
|
||||
# Check if 'configs' exists in this directory
|
||||
candidate = search_path / "configs"
|
||||
if candidate.exists() and candidate.is_dir():
|
||||
if (candidate / "prototype.json").exists():
|
||||
return candidate
|
||||
|
||||
# Also check if the current directory ITSELF is the configs dir we missed
|
||||
# (e.g. if we are somehow inside configs but missed check 1)
|
||||
if search_path.name == 'configs' and (search_path / "prototype.json").exists():
|
||||
return search_path
|
||||
|
||||
if search_path.parent == search_path: # Reached root
|
||||
break
|
||||
search_path = search_path.parent
|
||||
|
||||
# 4. Try finding 'orisys' package root via sys.modules (if loaded)
|
||||
try:
|
||||
if 'orisys' in sys.modules:
|
||||
mod = sys.modules['orisys']
|
||||
if hasattr(mod, '__file__') and mod.__file__:
|
||||
pkg_root = Path(mod.__file__).parent
|
||||
# pkg_root is .../site-packages/orisys
|
||||
candidate = pkg_root / "configs"
|
||||
if candidate.exists() and (candidate / "prototype.json").exists():
|
||||
return candidate
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to current path if nothing else works
|
||||
return current_path
|
||||
|
||||
CONFIG_DIR = _get_config_dir()
|
||||
|
||||
def load_config(config_name_or_path):
|
||||
"""
|
||||
Load sensor configuration from JSON file.
|
||||
|
||||
Accepts either:
|
||||
1. A file path (absolute or relative) to a JSON config file
|
||||
2. A config name (without .json extension) to load from package configs directory
|
||||
|
||||
Args:
|
||||
config_name_or_path: Either:
|
||||
- A file path string (e.g., "/path/to/config.json" or "./config.json")
|
||||
- A config name string (e.g., "prototype") to load from package configs
|
||||
|
||||
Returns:
|
||||
Dictionary containing the configuration
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the config file doesn't exist
|
||||
ValueError: If the file path is not a JSON file
|
||||
"""
|
||||
# Convert to Path object for easier handling
|
||||
config_path = Path(config_name_or_path)
|
||||
|
||||
# Check if it's an existing file (absolute or relative path)
|
||||
if config_path.exists() and config_path.is_file():
|
||||
# User provided a file path
|
||||
if config_path.suffix.lower() != '.json':
|
||||
raise ValueError(f"Config file must be a JSON file: {config_path}")
|
||||
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
# If it's an absolute path but doesn't exist, raise error immediately
|
||||
if config_path.is_absolute():
|
||||
raise FileNotFoundError(f"Config file not found at absolute path: {config_path}")
|
||||
|
||||
# Otherwise, treat it as a config name and look in package directory
|
||||
package_config_path = CONFIG_DIR / f"{config_name_or_path}.json"
|
||||
|
||||
if not package_config_path.exists():
|
||||
# List available configs for better error message
|
||||
available = [f.stem for f in CONFIG_DIR.glob("*.json")]
|
||||
available_str = ", ".join(available) if available else "none"
|
||||
raise FileNotFoundError(
|
||||
f"Input config file not found with name or path: {config_name_or_path}\n"
|
||||
f" Tried as file path: {config_path} (not found)\n"
|
||||
f" Tried as package config: {package_config_path} (not found)\n"
|
||||
f" Available package configs: {available_str}"
|
||||
)
|
||||
|
||||
with open(package_config_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
def list_config():
|
||||
"""
|
||||
List all available JSON configuration files in the configs directory.
|
||||
|
||||
Returns:
|
||||
List of configuration names (without .json extension), sorted alphabetically.
|
||||
Returns an empty list if no JSON files are found.
|
||||
"""
|
||||
config_files = sorted(CONFIG_DIR.glob("*.json"))
|
||||
return [f.stem for f in config_files]
|
||||
|
||||
def export_config(config_name = None, path="./config/exported_config.json"):
|
||||
"""
|
||||
Export configuration to JSON file with readable formatting.
|
||||
Arrays/lists are kept on a single line, while nested objects are indented.
|
||||
|
||||
Args:
|
||||
config: Configuration dictionary to export
|
||||
path: Output file path (default: "./config/exported_config.json")
|
||||
|
||||
Returns:
|
||||
The exported config dictionary
|
||||
"""
|
||||
config = load_config(config_name)
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
formatted = _format_json_value(config)
|
||||
f.write(formatted)
|
||||
f.write('\n') # Add trailing newline
|
||||
|
||||
print(f"Config name: {config_name} exported to {path}")
|
||||
return config
|
||||
|
||||
def _format_json_value(value, indent_level=0, indent_str=" "):
|
||||
"""
|
||||
Recursively format JSON value with arrays/lists on single line.
|
||||
|
||||
Args:
|
||||
value: The value to format (dict, list, or primitive)
|
||||
indent_level: Current indentation level
|
||||
indent_str: String used for indentation (default: 4 spaces)
|
||||
|
||||
Returns:
|
||||
Formatted string representation
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
if not value:
|
||||
return "{}"
|
||||
items = []
|
||||
for k, v in value.items():
|
||||
key_str = json.dumps(k, ensure_ascii=False)
|
||||
value_str = _format_json_value(v, indent_level + 1, indent_str)
|
||||
items.append(f"{indent_str * (indent_level + 1)}{key_str}: {value_str}")
|
||||
return "{\n" + ",\n".join(items) + "\n" + indent_str * indent_level + "}"
|
||||
elif isinstance(value, list):
|
||||
if not value:
|
||||
return "[]"
|
||||
# Format arrays/lists on a single line
|
||||
items = [json.dumps(item, ensure_ascii=False) for item in value]
|
||||
return "[" + ", ".join(items) + "]"
|
||||
else:
|
||||
# Primitive types (string, number, bool, None)
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"name": "prototype",
|
||||
"version": "0.3.1",
|
||||
"camera": {
|
||||
"resolution_raw": [1920, 1080],
|
||||
"fps": 30,
|
||||
"units": 15,
|
||||
"units_used": 9,
|
||||
"resolution_stitch": [365, 365],
|
||||
"stitch_res_list": [365,263,123,63]
|
||||
},
|
||||
"stitcher": {
|
||||
"min_area_frac": 0.01,
|
||||
"max_area_frac": 0.05,
|
||||
"max_aspect_ratio": 50,
|
||||
"index": [1,2,3,6,7,8,11,12,13],
|
||||
"margin": 10,
|
||||
"nominal_ov_pos": 100,
|
||||
"n": [3,3],
|
||||
"row_tol": 0.6,
|
||||
"box_size": [250, 250]
|
||||
},
|
||||
"tracker": {
|
||||
"adaptive": true,
|
||||
"lower_threshold": 12.0,
|
||||
"upper_threshold": 18.0,
|
||||
"loop_closure_interval": 1,
|
||||
"algorithm": "dis",
|
||||
"dis_preset": "medium",
|
||||
"dis_mode": 3,
|
||||
"algorithm_list": ["raft", "dis"],
|
||||
"hhd_downsample": 3
|
||||
},
|
||||
"contact": {
|
||||
"min_contour_area": 3000,
|
||||
"flow_threshold":5,
|
||||
"div_threshold":0.05,
|
||||
"expansion_threshold": 0.01,
|
||||
"kernel_size": [5,5],
|
||||
"denoise_sigma": 12
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+53
-22
@@ -200,7 +200,7 @@ class Sensor:
|
||||
Capture and preprocess a frame.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Processed grayscale image
|
||||
np.ndarray | None: Processed image, or None on capture failure.
|
||||
"""
|
||||
# Simple direct read - buffer size of 1 should minimize frame accumulation
|
||||
# Timing measurement helps identify if capture or processing is the bottleneck
|
||||
@@ -211,7 +211,7 @@ class Sensor:
|
||||
|
||||
if not ret:
|
||||
print("Failed to get_img from video source.")
|
||||
return False
|
||||
return None
|
||||
|
||||
# Start processing timing
|
||||
t_processing_start = time.perf_counter()
|
||||
@@ -250,41 +250,72 @@ class Sensor:
|
||||
self._fps = 1.0 / (time.perf_counter() - self.last_log_time)
|
||||
self.last_log_time = time.perf_counter()
|
||||
|
||||
return True
|
||||
return self.img.copy()
|
||||
|
||||
def detect_motion(self, threshold=6):
|
||||
|
||||
# detect motion by optical flow magnitude
|
||||
mag = np.linalg.norm(self.flow, axis=1)
|
||||
mag = np.linalg.norm(self.flow, axis=-1)
|
||||
# print(f"mag: {mag.max()}, {mag.min()}, {mag.mean()}")
|
||||
return mag.mean() > threshold
|
||||
|
||||
def compute_deformation(self, decompose=True, check_motion=True, threshold=6, tail_frames=10):
|
||||
"""
|
||||
Compute optical flow and decompose into force components.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Optical flow field
|
||||
Compute optical flow and optionally decompose it into force components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
decompose : bool
|
||||
If True, run HHD decomposition (`compute_hhd`) after flow computation.
|
||||
check_motion : bool
|
||||
If True, gate decomposition with `detect_motion(threshold)`. When motion
|
||||
falls below threshold, decomposition may still run for up to
|
||||
`tail_frames` additional frames to avoid abrupt force drop-off.
|
||||
threshold : float
|
||||
Motion-detection threshold on mean flow magnitude.
|
||||
tail_frames : int
|
||||
Number of extra non-motion frames allowed to keep running HHD when
|
||||
`check_motion=True`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool | None
|
||||
- `True` : motion is currently detected (`check_motion=True`)
|
||||
- `False` : motion is currently not detected (`check_motion=True`)
|
||||
- `None` : motion gating disabled (`check_motion=False`)
|
||||
"""
|
||||
if self.img is None:
|
||||
raise Exception("Warning: Get image first")
|
||||
|
||||
|
||||
self.compute_flow()
|
||||
# self.compute_hhd()
|
||||
if check_motion:
|
||||
if self.detect_motion(threshold=threshold):
|
||||
self.tail_frames_count = 0
|
||||
|
||||
if not check_motion:
|
||||
self.tail_frames_count = 0
|
||||
if decompose:
|
||||
self.compute_hhd()
|
||||
return True
|
||||
else:
|
||||
if self.tail_frames_count <= tail_frames:
|
||||
self.tail_frames_count +=1
|
||||
self.compute_hhd()
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
# return True
|
||||
|
||||
is_motion = self.detect_motion(threshold=threshold)
|
||||
try:
|
||||
tail_frames = max(0, int(tail_frames))
|
||||
except (TypeError, ValueError):
|
||||
tail_frames = 0
|
||||
|
||||
if is_motion:
|
||||
self.tail_frames_count = 0
|
||||
if decompose:
|
||||
self.compute_hhd()
|
||||
return True
|
||||
|
||||
# No motion this frame. Keep HHD alive briefly if requested, but do not
|
||||
# report this as "motion detected".
|
||||
if decompose and self.tail_frames_count < tail_frames:
|
||||
self.tail_frames_count += 1
|
||||
self.compute_hhd()
|
||||
else:
|
||||
self.tail_frames_count = tail_frames
|
||||
|
||||
return False
|
||||
|
||||
def compute_flow(self):
|
||||
if self.img is None:
|
||||
|
||||
Reference in New Issue
Block a user