clean up folder
@@ -12,7 +12,8 @@ orisys.egg-info/
|
||||
/build_nuitka
|
||||
/data/
|
||||
/models/
|
||||
/doc/.quarto/
|
||||
*.npy
|
||||
*.png
|
||||
SDK/
|
||||
|
||||
/patch_generation/exported_data/
|
||||
/docs/exported_data/
|
||||
/docs/.quarto/
|
||||
@@ -1,156 +0,0 @@
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import torch
|
||||
import torchvision.transforms.functional as F
|
||||
import torchvision.transforms as T
|
||||
from torchvision.models.optical_flow import raft_large, raft_small
|
||||
import numpy as np
|
||||
import time
|
||||
import orisys
|
||||
|
||||
def preprocess(batch, resize=None):
|
||||
"""
|
||||
Preprocess image batch for RAFT model.
|
||||
|
||||
Args:
|
||||
batch: Tensor of shape (N, C, H, W)
|
||||
resize: Optional tuple (height, width) to resize. If None, keeps original size.
|
||||
|
||||
Returns:
|
||||
Preprocessed tensor
|
||||
"""
|
||||
transform_list = [
|
||||
T.ConvertImageDtype(torch.float32),
|
||||
T.Normalize(mean=0.5, std=0.5), # map [0, 1] into [-1, 1]
|
||||
]
|
||||
|
||||
if resize is not None:
|
||||
transform_list.append(T.Resize(size=resize))
|
||||
|
||||
transforms = T.Compose(transform_list)
|
||||
batch = transforms(batch)
|
||||
return batch
|
||||
|
||||
def setup_simple_raft():
|
||||
"""
|
||||
Setup a simpler version using available models
|
||||
"""
|
||||
model = raft_small(pretrained=True, progress=False).to('cuda')
|
||||
model = model.eval()
|
||||
# Alternative: Use LiteFlowNet (lighter alternative)
|
||||
print("RAFT loaded successfully")
|
||||
return model
|
||||
|
||||
def track_raft(prev_frame, frame, model, device):
|
||||
|
||||
gray = frame.copy()
|
||||
start_time = time.time()
|
||||
prev_tensor = torch.from_numpy(prev_frame.transpose(2, 0, 1).astype(np.float32) / 255.0)
|
||||
curr_tensor = torch.from_numpy(gray.transpose(2, 0, 1).astype(np.float32) / 255.0)
|
||||
prev_tensor = preprocess(prev_tensor.unsqueeze(0), resize=None).to(device)
|
||||
curr_tensor = preprocess(curr_tensor.unsqueeze(0), resize=None).to(device)
|
||||
t0 = time.perf_counter()
|
||||
with torch.no_grad():
|
||||
flow = model(prev_tensor, curr_tensor)
|
||||
predicted_flows = flow[-1]
|
||||
t1 = time.perf_counter()
|
||||
print(f"RAFT time: {t1 - t0}")
|
||||
flow_np = predicted_flows[0].cpu().numpy() # or .squeeze(0)
|
||||
|
||||
# Transpose to (H, W, 2) format expected by draw_arrows
|
||||
flow_np = flow_np.transpose(1, 2, 0) # (2, H, W) -> (H, W, 2)
|
||||
|
||||
# torch.onnx.export(
|
||||
# model, # model to export
|
||||
# (prev_tensor, curr_tensor), # inputs of the model,
|
||||
# "my_model.onnx", # filename of the ONNX model
|
||||
# input_names=["prev_tensor", "curr_tensor"], # Rename inputs for the ONNX model
|
||||
# dynamo=True # True or False to select the exporter to use
|
||||
# )
|
||||
# print("ONNX model exported successfully")
|
||||
|
||||
return flow_np
|
||||
|
||||
def main_simple():
|
||||
# Initialize camera
|
||||
# sensor = orisys.Sensor('s1.mp4', config_name="./config/prototype_of3.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True)
|
||||
model = setup_simple_raft()
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
print(f"Using device: {device}")
|
||||
cap = cv2.VideoCapture("s1.mp4")
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
||||
|
||||
prev_gray = None
|
||||
fps_buffer = []
|
||||
|
||||
|
||||
while True:
|
||||
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
dx = 300
|
||||
dy = 0
|
||||
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
frame1 = frame[420:660, 840:1080, :]
|
||||
# frame2 = frame[420+dy:660+dy, 840+dx:1080+dx, :]
|
||||
# frame3 = frame[420-dy:660-dy, 840-dx:1080-dx, :]
|
||||
|
||||
# frame = np.concatenate([frame3, frame1, frame2], axis=1)
|
||||
frame = frame1
|
||||
# cv2.imshow("frame", frame3)
|
||||
# cv2.waitKey(0)
|
||||
if prev_gray is None:
|
||||
prev_gray = frame.copy()
|
||||
gray = frame
|
||||
# Compute optical flow
|
||||
start_time = time.time()
|
||||
flow_np = track_raft(prev_gray, gray, model, device)
|
||||
fps = 1.0 / (time.time() - start_time)
|
||||
fps_buffer.append(fps)
|
||||
if len(fps_buffer) > 30:
|
||||
fps_buffer.pop(0)
|
||||
|
||||
avg_fps = np.mean(fps_buffer)
|
||||
print(f"avg_fps: {avg_fps}")
|
||||
|
||||
|
||||
# # Remove batch dimension: (1, 2, H, W) -> (2, H, W)
|
||||
# flow_np = predicted_flows[0].cpu().numpy() # or .squeeze(0)
|
||||
|
||||
# # Transpose to (H, W, 2) format expected by draw_arrows
|
||||
# flow_np = flow_np.transpose(1, 2, 0) # (2, H, W) -> (H, W, 2)
|
||||
|
||||
# Flow is already in pixel coordinates, no denormalization needed
|
||||
# Keep as float32 for draw_arrows (it can handle float values)
|
||||
print(f"Flow numpy shape: {flow_np.shape}, dtype: {flow_np.dtype}")
|
||||
print(f"Flow range: x=[{flow_np[:,:,0].min():.2f}, {flow_np[:,:,0].max():.2f}], "
|
||||
f"y=[{flow_np[:,:,1].min():.2f}, {flow_np[:,:,1].max():.2f}]")
|
||||
|
||||
arrows = orisys.util.draw_arrows(
|
||||
gray,
|
||||
flow_np,
|
||||
threshold=2,
|
||||
grid_spacing=40,
|
||||
arrow_scale=1.0
|
||||
)
|
||||
|
||||
arrows = cv2.resize(arrows, (400, 400))
|
||||
cv2.imshow('Optical Flow', arrows)
|
||||
|
||||
# prev_gray = gray.copy()
|
||||
|
||||
#步骤 3.5: 键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
prev_gray = None
|
||||
print("传感器已重置")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main_simple()
|
||||
@@ -0,0 +1,85 @@
|
||||
# Orisys SDK
|
||||
|
||||
Orisys SDK is a Python toolkit for optical tactile sensing. It provides image capture,
|
||||
stitching, dense optical flow, Helmholtz-Hodge decomposition, contact/depth estimation,
|
||||
and several runnable examples for camera and video-based workflows.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
- `src/orisys/`: installable SDK package
|
||||
- `config/`: default and lab/runtime JSON configurations
|
||||
- `examples/`: user-facing example scripts
|
||||
- `scripts/`: maintenance, calibration, packaging, and demo scripts
|
||||
- `tools/`: non-package tooling such as patch-generation utilities
|
||||
- `tests/`: automated pytest suite
|
||||
- `docs/`: usage and API documentation
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create an environment
|
||||
|
||||
```bash
|
||||
conda create -n orisys python=3.10
|
||||
conda activate orisys
|
||||
```
|
||||
|
||||
### 2. Install the SDK in editable mode
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
Optional GPU extras:
|
||||
|
||||
```bash
|
||||
pip install -e .[cuda12]
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
pip install -e .[cuda13]
|
||||
```
|
||||
|
||||
### 3. Run tests
|
||||
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
### 4. Run an example
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
The canonical configuration files now live in the repo-level `config/` folder.
|
||||
The package loader `orisys.configs` resolves configs from:
|
||||
|
||||
1. an explicit JSON file path
|
||||
2. a config file stem under `config/`
|
||||
3. a backward-compatible JSON `"name"` match, such as `"prototype"`
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
import orisys
|
||||
|
||||
sensor = orisys.Sensor(0, config_name="ddjx01")
|
||||
cfg = orisys.export_config("ddjx01", "./config/exported_config.json")
|
||||
```
|
||||
|
||||
## Examples and Tools
|
||||
|
||||
- Example entry points: `examples/README.md`
|
||||
- User documentation: `docs/get_started.md`
|
||||
- Packaging helper: `scripts/build_nuitka.py`
|
||||
- Specialized utilities: `tools/`
|
||||
|
||||
## Notes for Collaborators
|
||||
|
||||
- Generated build outputs such as `build/`, `dist/`, and `SDK/` are intentionally ignored.
|
||||
- Large or generated tooling outputs should stay under ignored output folders rather than be committed.
|
||||
- If you add new images or arrays that should be versioned, review `.gitignore` first.
|
||||
@@ -1,195 +0,0 @@
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Calibration Data Collection')
|
||||
parser.add_argument('-i','--vid_source', type=str, default=0, help='Video Source')
|
||||
parser.add_argument('-id','--device_id', type=int, default=0, help='Device ID')
|
||||
|
||||
args = parser.parse_args()
|
||||
device_id = args.device_id
|
||||
vid_source = args.vid_source
|
||||
sample_name = f"dd01a_{device_id}" #dd01 (平面款), dd01a (平面款 手搓), dd02 (手指款)
|
||||
test_type = "normal" # shearX, shearY, normal
|
||||
print("\n")
|
||||
print(f"Collecting data for device ID: {device_id} on test type: {test_type}, video source: {vid_source}")
|
||||
print("\n")
|
||||
sensor = orisys.Sensor(vid_source, config_name="./config/prototype_of3.json", cal_path= f"./config/{sample_name}.npy", cuda=False, verbose=True)
|
||||
|
||||
print(f"\n按 'q' 退出程序,按 'r' 重置光流追踪器,按 's' 开始保存,按 'e' 停止保存,按 'j' 减小力,按 'l' 增大力")
|
||||
force = 0 # kgf
|
||||
force_step = 0.1 # kgf
|
||||
force_min = 0 # kgf
|
||||
force_max = 3 # kgf
|
||||
|
||||
is_save = False
|
||||
save_mode = ["raw", "stitched", "arrows", "contact", "depth"]
|
||||
save_mode_index = 0
|
||||
frame_count = 0
|
||||
writer = None
|
||||
save_path = None
|
||||
|
||||
while True:
|
||||
sensor.get_img()
|
||||
|
||||
if is_save and writer is not None and sensor.frame is not None:
|
||||
frame_count += 1
|
||||
if save_mode_index == 0:
|
||||
save_img = sensor.frame
|
||||
elif save_mode_index == 1:
|
||||
save_img = sensor.img
|
||||
elif save_mode_index == 2:
|
||||
save_img = arrows
|
||||
elif save_mode_index == 3:
|
||||
save_img = image_with_foe
|
||||
elif save_mode_index == 4:
|
||||
save_img = div_normalized
|
||||
|
||||
# Convert grayscale (1 channel) to BGR (3 channels) if needed
|
||||
if len(save_img.shape) == 2 or (len(save_img.shape) == 3 and save_img.shape[2] == 1):
|
||||
if len(save_img.shape) == 3:
|
||||
save_img = save_img.squeeze(2)
|
||||
save_img = cv2.cvtColor(save_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
writer.write(save_img)
|
||||
|
||||
sensor.compute_deformation()
|
||||
sensor.compute_contact()
|
||||
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.VNORMAL,
|
||||
sensor.info.IMG,
|
||||
sensor.info.CONTOUR,
|
||||
sensor.info.CENTROID,
|
||||
sensor.info.DEPTH,
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY
|
||||
)
|
||||
|
||||
arrows = orisys.util.draw_arrows(
|
||||
img,
|
||||
flow,
|
||||
threshold=4,
|
||||
grid_spacing=10,
|
||||
arrow_scale=1.0
|
||||
)
|
||||
cv2.putText(arrows, f"FPS: {fps:.0f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
|
||||
cv2.imshow("Deformation", arrows)
|
||||
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("Contact Centroid", image_with_foe)
|
||||
|
||||
div_abs = depth_map
|
||||
if div_abs.max() > div_abs.min():
|
||||
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
||||
else:
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
|
||||
if key == ord("s"):
|
||||
save_name = f"{sample_name}_{test_type}_{force:.1f}kgf"
|
||||
save_path = f"./data/{sample_name}/{test_type}/{save_name}.mp4"
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# Get frame dimensions from the actual frame
|
||||
save_img = None
|
||||
if save_mode_index == 0:
|
||||
save_img = sensor.frame
|
||||
elif save_mode_index == 1:
|
||||
save_img = sensor.img
|
||||
elif save_mode_index == 2:
|
||||
save_img = arrows
|
||||
elif save_mode_index == 3:
|
||||
save_img = image_with_foe
|
||||
elif save_mode_index == 4:
|
||||
save_img = div_normalized
|
||||
|
||||
if save_img is not None:
|
||||
# Convert grayscale (1 channel) to BGR (3 channels) if needed for dimension check
|
||||
temp_img = save_img.copy()
|
||||
if len(temp_img.shape) == 2 or (len(temp_img.shape) == 3 and temp_img.shape[2] == 1):
|
||||
if len(temp_img.shape) == 3:
|
||||
temp_img = temp_img.squeeze(2)
|
||||
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
frame_height, frame_width = temp_img.shape[:2]
|
||||
writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (frame_width, frame_height))
|
||||
if writer.isOpened():
|
||||
is_save = True
|
||||
print(f"Start writing to {save_path}")
|
||||
else:
|
||||
print(f"Failed to open video writer for {save_path}")
|
||||
writer = None
|
||||
else:
|
||||
print("No frame available to determine video dimensions")
|
||||
|
||||
if key == ord("e"):
|
||||
is_save = False
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
if save_path is not None:
|
||||
print(f"Stop writing to {save_path}, total frames: {frame_count}")
|
||||
else:
|
||||
print(f"Stop writing, total frames: {frame_count}")
|
||||
frame_count = 0
|
||||
|
||||
if key == ord("p"):
|
||||
print(f"Normal Force: {fnormal:.1f} kgf, Shear ForceX: {fshearx:.1f} kgf, Shear ForceY: {fsheary:.1f} kgf")
|
||||
|
||||
if key == ord("i"):
|
||||
save_mode_index += 1
|
||||
if save_mode_index >= len(save_mode):
|
||||
save_mode_index = 0
|
||||
print(f"Save mode: {save_mode[save_mode_index]}")
|
||||
|
||||
if key == ord("k"):
|
||||
save_mode_index -= 1
|
||||
if save_mode_index < 0:
|
||||
save_mode_index = len(save_mode) - 1
|
||||
print(f"Save mode: {save_mode[save_mode_index]}")
|
||||
if key == ord("j"):
|
||||
force -= force_step
|
||||
if force < force_min:
|
||||
force = force_min
|
||||
print(f"Force: {force:.1f} kgf")
|
||||
if key == ord("l"):
|
||||
force += force_step
|
||||
if force > force_max:
|
||||
force = force_max
|
||||
print(f"Force: {force:.1f} kgf")
|
||||
|
||||
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# 4. 清理资源
|
||||
# =========================================================================
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
sensor.disconnect() # 释放视频源和相关资源
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
After Width: | Height: | Size: 2.0 MiB |
@@ -72,8 +72,8 @@ conda install conda-forge::pyvkfft
|
||||
### 输入参数
|
||||
|
||||
* **vid_src** (`int | str`, 必填): 传感器 ID、相机编号或视频路径。默认为 `0`。
|
||||
* **config_name** (`str`,可选): 传感器配置`json`文件路径,用于加载对应的参数配置,默认为 `./config/ddjx01`。
|
||||
* * **cal_path** (`str`,可选,已弃用): 该参数已弃用,当前版本中不再使用,传入后也会被忽略。为兼容旧代码暂时保留。: 复眼拼接校准文件路径,默认为 `./config/cal.npy`。有多个传感器同时运行时建议传感器类创建时建议填写,避免校准文件覆写。在首次运行时程序会在找不到文件时自动创建。
|
||||
* **config_name** (`str`,可选): 传感器配置文件名称或 `json` 文件路径,用于加载对应的参数配置;例如 `ddjx01` 或 `./config/ddjx01.json`。
|
||||
* **cal_path** (`str`,可选,已弃用): 该参数仅为兼容旧代码保留,当前版本中不再使用,传入后也会被忽略。
|
||||
* **backend** (`str`,可选): 光流场解耦计算后端,默认为 `auto`,可选`cupy`,`opencl`,`cpu`。
|
||||
* **verbose** (`bool`,可选): 是否输出初始化及运行过程中的详细日志信息,默认为 `False`。
|
||||
|
||||
@@ -120,7 +120,7 @@ print(orisys.__version__)
|
||||
3. 如果第一步没有输出,传感器的ID就是“0”,如果第一步有一个设备,传感器的ID就是“1”,如此类推。
|
||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开Anaconda Prompt, 切换到工作路径,然后在命令行中输入:
|
||||
``` bash
|
||||
python example1_basic_pipeline.py -v 0
|
||||
python examples/basic_pipeline.py -v 0
|
||||
```
|
||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
||||
|
||||
@@ -130,7 +130,7 @@ print(orisys.__version__)
|
||||
3. 根据新增的输出,例如”/dev/video0”,确定传感器的ID为“0”, 如果是”/dev/video3”的话ID为“3” ,如此类推。
|
||||
4. 在确定了传感器的ID(下方例子假设为“0”)后,打开终端, 激活相应的python环境,切换到工作路径,然后在命令行中输入:
|
||||
``` bash
|
||||
python example1_basic_pipeline.py -v 0
|
||||
python examples/basic_pipeline.py -v 0
|
||||
```
|
||||
5. 程序会运行,并自动打开可视化窗口和打印资料,在窗口上按键盘”q”键可以停止运行,按”r”键可以重设数据
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
@@ -0,0 +1,50 @@
|
||||
# Examples
|
||||
|
||||
This folder contains user-facing example scripts for the Orisys SDK.
|
||||
|
||||
## Files
|
||||
|
||||
- `basic_pipeline.py`: single-sensor basic processing and visualization flow
|
||||
- `two_sensor_pipeline.py`: dual-sensor multiprocessing example
|
||||
- `plot_data.py`: real-time plotting example with PyQtGraph
|
||||
|
||||
## Typical usage
|
||||
|
||||
Run examples from the repository root so relative paths such as `./config/ddjx01.json`
|
||||
resolve correctly.
|
||||
|
||||
### Basic pipeline
|
||||
|
||||
```bash
|
||||
python examples/basic_pipeline.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
|
||||
### Two-sensor pipeline
|
||||
|
||||
```bash
|
||||
python examples/two_sensor_pipeline.py
|
||||
```
|
||||
|
||||
### Real-time plotting
|
||||
|
||||
```bash
|
||||
python examples/plot_data.py -v 0 -c ./config/ddjx01.json
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python environment with the SDK dependencies installed
|
||||
- a supported camera or test video
|
||||
- valid JSON config in `./config/`
|
||||
|
||||
Some examples also require:
|
||||
|
||||
- `pyqtgraph`
|
||||
- `PyQt5`
|
||||
- multiple cameras for multi-sensor demos
|
||||
|
||||
## Notes
|
||||
|
||||
- Most examples assume a tactile sensor camera is connected.
|
||||
- Press `q` in the display window to quit where applicable.
|
||||
- If your camera index is not `0`, change `-v` accordingly.
|
||||
@@ -42,10 +42,10 @@ def main():
|
||||
# 步骤 2:数据处理
|
||||
# 步骤 2.1:获取并拼接图像(必需)
|
||||
sensor.get_img()
|
||||
# 步骤 2.2:计算形变场(必需)
|
||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
|
||||
# 步骤 2.3:检测到接触后再计算接触区域
|
||||
if is_touched:
|
||||
# 步骤 2.2:计算形变场(必需);返回值表示当前帧是否检测到接触/运动
|
||||
is_contact_now = sensor.compute_deformation(check_motion=True, threshold=6)
|
||||
# 步骤 2.3:仅在当前帧检测到接触/运动时计算接触区域
|
||||
if is_contact_now:
|
||||
sensor.compute_contact()
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ def main():
|
||||
|
||||
writer.write(save_img)
|
||||
|
||||
sensor.compute_deformation(check_motion=False) # 计算形变与分解结果
|
||||
sensor.compute_deformation(check_motion=True, threshold=0) # 计算形变与分解结果
|
||||
# 读取信息:
|
||||
# 向量场:VRAW、VNORMAL、VSHEAR -> 原始 / 法向 / 切向
|
||||
# 标量:FNORMAL、FSHEARX、FSHEARY -> 法向力 / X 向切向力 / Y 向切向力
|
||||
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -1,217 +0,0 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Utility functions from the original flat pattern generator
|
||||
# (unchanged, except we now work with 0‑1 floats)
|
||||
# ----------------------------------------------------------------------
|
||||
def merge_intervals(intervals):
|
||||
if not intervals:
|
||||
return []
|
||||
intervals.sort(key=lambda x: x[0])
|
||||
merged = [intervals[0]]
|
||||
for current in intervals[1:]:
|
||||
last = merged[-1]
|
||||
if current[0] <= last[1]:
|
||||
merged[-1] = (last[0], max(last[1], current[1]))
|
||||
else:
|
||||
merged.append(current)
|
||||
return merged
|
||||
|
||||
def get_allowed_intervals(neighbor_values, delta):
|
||||
forbidden = []
|
||||
for c in neighbor_values:
|
||||
low = max(0, c - delta)
|
||||
high = min(1, c + delta)
|
||||
forbidden.append((low, high))
|
||||
merged = merge_intervals(forbidden)
|
||||
allowed = []
|
||||
if merged:
|
||||
if merged[0][0] > 0:
|
||||
allowed.append((0, merged[0][0]))
|
||||
for i in range(1, len(merged)):
|
||||
allowed.append((merged[i-1][1], merged[i][0]))
|
||||
if merged[-1][1] < 1:
|
||||
allowed.append((merged[-1][1], 1))
|
||||
else:
|
||||
allowed.append((0, 1))
|
||||
return allowed
|
||||
|
||||
def sample_from_allowed(allowed):
|
||||
lengths = [end - start for start, end in allowed]
|
||||
total_length = sum(lengths)
|
||||
if total_length <= 0:
|
||||
return None
|
||||
point = np.random.rand() * total_length
|
||||
cumulative = 0
|
||||
for (start, end), length in zip(allowed, lengths):
|
||||
cumulative += length
|
||||
if point <= cumulative:
|
||||
return np.random.uniform(start, end)
|
||||
return allowed[-1][0]
|
||||
|
||||
def sample_channel(neighbor_values, delta):
|
||||
allowed = get_allowed_intervals(neighbor_values, delta)
|
||||
if not allowed:
|
||||
return None
|
||||
return sample_from_allowed(allowed)
|
||||
|
||||
def generate_valid_color(neighbors, r):
|
||||
delta = np.sqrt(r)
|
||||
color = np.zeros(3)
|
||||
for channel in range(3):
|
||||
neighbor_channel = [n[channel] for n in neighbors]
|
||||
sampled = sample_channel(neighbor_channel, delta)
|
||||
if sampled is None:
|
||||
sampled = np.random.rand()
|
||||
color[channel] = sampled
|
||||
return color
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Cylinder‑specific generator
|
||||
# ----------------------------------------------------------------------
|
||||
def generate_color_pattern_cylinder(
|
||||
R=9.0, # cylinder radius (mm)
|
||||
H=20.0, # cylinder height (mm)
|
||||
fov_deg=150.0, # vertical FOV (degrees)
|
||||
pixels_per_mm=100, # texture resolution
|
||||
n_azimuth=36, # number of patches around the circumference
|
||||
n_radial=20, # number of patches vertically (visible part)
|
||||
r=0.2, # color randomness regulation factor
|
||||
output_path='cylinder_pattern.png'
|
||||
):
|
||||
"""
|
||||
Generate a texture map for the inner cylinder wall.
|
||||
Patches are arranged so that, from a camera at the bottom
|
||||
looking upward with perspective projection, they appear
|
||||
uniform in angular and radial extent.
|
||||
"""
|
||||
|
||||
# Camera half‑angle (rad)
|
||||
half_fov = np.radians(fov_deg / 2.0)
|
||||
|
||||
# Visible height range (z_min = lowest point seen by camera)
|
||||
z_min = R / np.tan(half_fov) if half_fov < np.pi/2 else 0.0
|
||||
z_max = H
|
||||
|
||||
# Image plane radial coordinate r' = R / z
|
||||
r_top = R / z_max # smallest r' (top)
|
||||
r_bottom = R / z_min # largest r' (bottom)
|
||||
|
||||
# Define patch grid in image space
|
||||
theta_step = 2 * np.pi / n_azimuth
|
||||
r_step = (r_bottom - r_top) / n_radial
|
||||
|
||||
# Texture dimensions
|
||||
width_px = int(np.round(2 * np.pi * R * pixels_per_mm))
|
||||
height_px = int(np.round(H * pixels_per_mm))
|
||||
print(f"Texture size: {width_px} x {height_px} pixels")
|
||||
|
||||
# Store patch colors in a 2D array (rows = radial index, columns = azimuth)
|
||||
patch_colors = np.zeros((n_radial, n_azimuth, 3), dtype=np.float32)
|
||||
|
||||
# Generate colors row by row (top to bottom)
|
||||
for j in range(n_radial): # j = 0 → top, j = n_radial-1 → bottom
|
||||
r_start = r_top + j * r_step
|
||||
r_end = r_start + r_step
|
||||
# Corresponding z values (note: r increases → z decreases)
|
||||
z_start = R / r_start # larger z (higher up)
|
||||
z_end = R / r_end # smaller z (lower down)
|
||||
v_start = z_start / H
|
||||
v_end = z_end / H
|
||||
# Ensure v_start > v_end (top has larger v)
|
||||
if v_start < v_end:
|
||||
v_start, v_end = v_end, v_start
|
||||
|
||||
for i in range(n_azimuth):
|
||||
theta_start = i * theta_step
|
||||
theta_end = theta_start + theta_step
|
||||
u_start = theta_start / (2 * np.pi)
|
||||
u_end = theta_end / (2 * np.pi)
|
||||
|
||||
# Collect neighbor colors (above and left)
|
||||
neighbors = []
|
||||
# Above neighbor (previous row, same column)
|
||||
if j > 0:
|
||||
neighbors.append(patch_colors[j-1, i])
|
||||
# Left neighbor (same row, previous column) – wrap around
|
||||
left_i = (i - 1) % n_azimuth
|
||||
if i > 0 or j > 0: # at least one neighbor exists
|
||||
neighbors.append(patch_colors[j, left_i])
|
||||
|
||||
if len(neighbors) == 0:
|
||||
new_color = np.random.rand(3)
|
||||
else:
|
||||
new_color = generate_valid_color(neighbors, r)
|
||||
|
||||
patch_colors[j, i] = new_color
|
||||
|
||||
# Render the texture image
|
||||
img = Image.new('RGB', (width_px, height_px), color=(255, 255, 255))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for j in range(n_radial):
|
||||
r_start = r_top + j * r_step
|
||||
r_end = r_start + r_step
|
||||
z_start = R / r_start
|
||||
z_end = R / r_end
|
||||
|
||||
# Determine v for top and bottom edges
|
||||
v_top = max(z_start, z_end) / H # larger z -> higher on cylinder
|
||||
v_bottom = min(z_start, z_end) / H # smaller z -> lower on cylinder
|
||||
|
||||
for i in range(n_azimuth):
|
||||
u_start = i * theta_step / (2 * np.pi)
|
||||
u_end = (i+1) * theta_step / (2 * np.pi)
|
||||
|
||||
# Pixel coordinates (ensure integer ordering)
|
||||
x0 = int(u_start * width_px)
|
||||
x1 = int(u_end * width_px)
|
||||
y0 = int(v_bottom * height_px)
|
||||
y1 = int(v_top * height_px)
|
||||
|
||||
# Sanity: x0 < x1, y0 < y1
|
||||
if x0 > x1:
|
||||
x0, x1 = x1, x0
|
||||
if y0 > y1:
|
||||
y0, y1 = y1, y0
|
||||
|
||||
# Avoid degenerate rectangles (patch may span seam)
|
||||
if x1 <= x0:
|
||||
x1 = x0 + 1
|
||||
if y1 <= y0:
|
||||
y1 = y0 + 1
|
||||
|
||||
color = tuple((patch_colors[j, i] * 255).astype(np.uint8))
|
||||
draw.rectangle([x0, y0, x1, y1], fill=color)
|
||||
|
||||
img.save(output_path)
|
||||
print(f"Saved texture to {output_path}")
|
||||
|
||||
# Also return as numpy array (BGR for OpenCV)
|
||||
img_np = np.array(img)[:, :, ::-1] # RGB -> BGR
|
||||
return img_np
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Example usage
|
||||
# ----------------------------------------------------------------------
|
||||
if __name__ == '__main__':
|
||||
# Generate pattern with 36 azimuth patches and 20 radial patches
|
||||
patch_size = 12
|
||||
R = 9.0
|
||||
H = 20.0
|
||||
fov_deg = 150.0
|
||||
pixels_per_mm = 100
|
||||
n_azimuth = int(2*np.pi*R*pixels_per_mm / patch_size)
|
||||
n_radial = int(H*pixels_per_mm / patch_size)
|
||||
r = 0.05
|
||||
pattern = generate_color_pattern_cylinder(
|
||||
R=9.0, H=20.0, fov_deg=150.0, pixels_per_mm=100,
|
||||
n_azimuth=n_azimuth, n_radial=n_radial, r=0.05
|
||||
)
|
||||
|
||||
# Display with OpenCV
|
||||
cv2.imshow('Cylinder Color Pattern', pattern)
|
||||
cv2.waitKey(0)
|
||||
cv2.destroyAllWindows()
|
||||
|
Before Width: | Height: | Size: 344 KiB |
|
Before Width: | Height: | Size: 912 KiB |
|
Before Width: | Height: | Size: 429 KiB |
@@ -1,224 +0,0 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
def calculate_local_contrast(image_input, window_size=5):
|
||||
"""
|
||||
Calculate Global Average Local Contrast (for fine texture detail evaluation in optical flow).
|
||||
Args:
|
||||
image_input: str (image file path) or numpy array (grayscale/RGB image).
|
||||
window_size: int (odd number, default=5) — sliding window size for fine texture analysis.
|
||||
Returns:
|
||||
float: Global average local contrast (higher = better fine texture detail; target ≥0.2 for optical flow).
|
||||
Raises:
|
||||
ValueError: If image is invalid, window size is even/too small, or processing fails.
|
||||
"""
|
||||
# ---------------------- Step 1: Input Validation & Preprocessing ----------------------
|
||||
# Validate window size (must be odd, ≥3 to capture meaningful texture)
|
||||
if window_size % 2 == 0 or window_size < 3:
|
||||
raise ValueError(f"Window size must be an odd integer ≥3 (got {window_size}). Use 3/5/7.")
|
||||
|
||||
# Load/convert image to grayscale (consistent with optical flow workflow)
|
||||
if isinstance(image_input, str):
|
||||
img = cv2.imread(image_input)
|
||||
if img is None:
|
||||
raise ValueError(f"Failed to load image from path: {image_input}")
|
||||
elif isinstance(image_input, np.ndarray):
|
||||
img = image_input.copy()
|
||||
else:
|
||||
raise ValueError("Input must be an image file path (str) or numpy array (image).")
|
||||
|
||||
# Convert to grayscale (handle RGB/BGR)
|
||||
if len(img.shape) == 3:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = img # Already grayscale
|
||||
|
||||
# Normalize to 0-255 (avoid non-standard pixel ranges, e.g., float32)
|
||||
gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
||||
|
||||
# ---------------------- Step 2: Sliding Window Local Contrast Calculation ----------------------
|
||||
# Define padding (replicate edge pixels to avoid boundary distortion)
|
||||
pad_size = window_size // 2
|
||||
gray_padded = cv2.copyMakeBorder(
|
||||
gray,
|
||||
top=pad_size, bottom=pad_size, left=pad_size, right=pad_size,
|
||||
borderType=cv2.BORDER_REPLICATE
|
||||
)
|
||||
|
||||
# Initialize array to store local contrast for each window
|
||||
local_contrast_vals = []
|
||||
|
||||
# Iterate over every pixel (center of the sliding window)
|
||||
for y in range(pad_size, gray_padded.shape[0] - pad_size):
|
||||
for x in range(pad_size, gray_padded.shape[1] - pad_size):
|
||||
# Extract 5x5 (or custom) window around (x,y)
|
||||
window = gray_padded[y-pad_size : y+pad_size+1, x-pad_size : x+pad_size+1]
|
||||
|
||||
# Compute Michelson contrast for the window (avoid division by zero)
|
||||
window_max = np.max(window)
|
||||
window_min = np.min(window)
|
||||
if window_max + window_min == 0: # All pixels are 0 (black window)
|
||||
lc = 0.0
|
||||
else:
|
||||
lc = (window_max - window_min) / (window_max + window_min)
|
||||
|
||||
local_contrast_vals.append(lc)
|
||||
|
||||
# ---------------------- Step 3: Global Average Local Contrast ----------------------
|
||||
global_local_contrast = np.mean(local_contrast_vals) if local_contrast_vals else 0.0
|
||||
|
||||
# Round for readability (4 decimal places)
|
||||
return global_local_contrast
|
||||
|
||||
def calculate_edge_strength_density(image_input):
|
||||
"""
|
||||
Calculate Edge Strength and Edge Density for optical flow evaluation.
|
||||
Args:
|
||||
image_input: Can be (1) str (image file path) or (2) numpy array (grayscale/RGB image).
|
||||
Returns:
|
||||
dict: Contains two metrics:
|
||||
- edge_strength: Average gradient magnitude of valid edges (higher = sharper edges).
|
||||
- edge_density: Percentage of valid edge pixels (higher = more texture/edges).
|
||||
Raises:
|
||||
ValueError: If image cannot be loaded/processed.
|
||||
"""
|
||||
# ---------------------- Step 1: Load/Preprocess Image ----------------------
|
||||
|
||||
img = image_input
|
||||
# Convert to grayscale (handle RGB/BGR)
|
||||
if len(img.shape) == 3:
|
||||
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = img # Already grayscale
|
||||
|
||||
# Normalize to 0-255 (avoid float/non-standard pixel ranges)
|
||||
gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
||||
|
||||
# ---------------------- Step 2: Compute Gradient Magnitude (Sobel) ----------------------
|
||||
# Sobel kernel size = 3 (balance between noise reduction and edge sharpness for optical flow)
|
||||
sobel_x = cv2.Sobel(gray, cv2.CV_64F, 1, 0, ksize=3) # Horizontal gradient
|
||||
sobel_y = cv2.Sobel(gray, cv2.CV_64F, 0, 1, ksize=3) # Vertical gradient
|
||||
|
||||
# Compute gradient magnitude (edge strength raw values)
|
||||
gradient_magnitude = np.sqrt(sobel_x**2 + sobel_y**2)
|
||||
|
||||
# ---------------------- Step 3: Adaptive Threshold for Valid Edges ----------------------
|
||||
# Use median gradient to filter noise (robust to outliers)
|
||||
median_gradient = np.median(gradient_magnitude)
|
||||
# Threshold = 1.5× median (filters weak noise, keeps meaningful edges for optical flow)
|
||||
edge_threshold = 1.5 * median_gradient
|
||||
# Create binary mask for valid edges (1 = valid edge, 0 = non-edge)
|
||||
valid_edge_mask = (gradient_magnitude > edge_threshold).astype(np.uint8)
|
||||
|
||||
# ---------------------- Step 4: Calculate Metrics ----------------------
|
||||
# Edge Strength: Average gradient magnitude of valid edges
|
||||
valid_gradient_values = gradient_magnitude[valid_edge_mask == 1]
|
||||
edge_strength = np.mean(valid_gradient_values) if len(valid_gradient_values) > 0 else 0.0
|
||||
|
||||
# Edge Density: Percentage of valid edge pixels (total pixels = height × width)
|
||||
total_pixels = gray.shape[0] * gray.shape[1]
|
||||
valid_edge_pixels = np.sum(valid_edge_mask)
|
||||
edge_density = (valid_edge_pixels / total_pixels) * 100 if total_pixels > 0 else 0.0
|
||||
# print(f"edge_strength: {edge_strength}, edge_density: {edge_density}")
|
||||
|
||||
# Round for readability (3 decimal places)
|
||||
return edge_strength, edge_density
|
||||
|
||||
def custom_gray_conversion(img, r_weight=0.299, g_weight=0.587, b_weight=0.114,
|
||||
red_boost=1.2, green_reduce=0.8):
|
||||
"""
|
||||
Custom grayscale calculation with tunable RGB weights + red/green contrast enhancement
|
||||
Args:
|
||||
image_path: Path to input image
|
||||
r_weight: Weight for Red channel (default: 0.299)
|
||||
g_weight: Weight for Green channel (default: 0.587)
|
||||
b_weight: Weight for Blue channel (default: 0.114)
|
||||
red_boost: Multiplier to boost red region grayscale (>1 = brighter)
|
||||
green_reduce: Multiplier to reduce green region grayscale (<1 = darker)
|
||||
Returns:
|
||||
default_gray: Grayscale with standard weights
|
||||
custom_gray: Grayscale with tunable weights + red/green enhancement
|
||||
"""
|
||||
# 1. Read image and validate
|
||||
# img_bgr = cv2.imread(image_path)
|
||||
img_bgr = img
|
||||
|
||||
if img_bgr is None:
|
||||
raise ValueError("Failed to read image - check path!")
|
||||
|
||||
# 2. Convert BGR (OpenCV default) to RGB and split channels
|
||||
img_rgb = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)
|
||||
# Convert to float32 to avoid integer overflow/precision loss
|
||||
r = img_rgb[:, :, 0].astype(np.float32)
|
||||
g = img_rgb[:, :, 1].astype(np.float32)
|
||||
b = img_rgb[:, :, 2].astype(np.float32)
|
||||
|
||||
# 3. Calculate DEFAULT grayscale (standard weights)
|
||||
default_gray = 0.299 * r + 0.587 * g + 0.114 * b
|
||||
|
||||
# 4. Calculate CUSTOM grayscale (tunable weights)
|
||||
custom_gray_base = r_weight * r + g_weight * g + b_weight * b
|
||||
|
||||
# 5. Enhance red/green contrast (optional but effective)
|
||||
# Define red/green regions using RGB value ratios (no HSV needed)
|
||||
# red_mask = (r > g * 1.2) & (r > b * 1.2) & (r >= 40) # Red-dominant pixels
|
||||
# green_mask = (g > r * 1.2) & (g > b * 1.2) & (g >= 40) # Green-dominant pixels
|
||||
|
||||
# Apply targeted adjustments to red/green regions
|
||||
custom_gray = custom_gray_base.copy()
|
||||
# custom_gray[red_mask] *= red_boost # Boost red region grayscale
|
||||
# custom_gray[green_mask] *= green_reduce # Reduce green region grayscale
|
||||
|
||||
# 6. Normalize to 0-255 range and convert back to uint8 (OpenCV standard)
|
||||
# Clip values first to avoid overflow, then scale if needed
|
||||
default_gray = np.clip(default_gray, 0, 255).astype(np.uint8)
|
||||
custom_gray = np.clip(custom_gray, 0, 255).astype(np.uint8)
|
||||
|
||||
return default_gray, custom_gray
|
||||
|
||||
# -------------------------- Test the Code --------------------------
|
||||
if __name__ == "__main__":
|
||||
# Replace with your image path
|
||||
IMAGE_PATH = "./raw_1.jpg"
|
||||
img = cv2.imread(IMAGE_PATH)
|
||||
dx = 50
|
||||
dy = 0
|
||||
tile = img[410+dy:650+dy,940+dx:1180+dx,:]
|
||||
|
||||
# Example 1: Use standard weights (verify correctness)
|
||||
default_gray, red_gray = custom_gray_conversion(
|
||||
tile,
|
||||
r_weight=1, g_weight=0, b_weight=0
|
||||
)
|
||||
|
||||
default_gray, green_gray = custom_gray_conversion(
|
||||
tile,
|
||||
r_weight=0, g_weight=1, b_weight=0
|
||||
)
|
||||
default_gray, blue_gray = custom_gray_conversion(
|
||||
tile,
|
||||
r_weight=0, g_weight=0, b_weight=1
|
||||
)
|
||||
|
||||
edge_strength, edge_density = calculate_edge_strength_density(default_gray)
|
||||
local_contrast = calculate_local_contrast(default_gray)
|
||||
print(f"标准灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}")
|
||||
edge_strength, edge_density = calculate_edge_strength_density(red_gray)
|
||||
local_contrast = calculate_local_contrast(red_gray)
|
||||
print(f"红色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}")
|
||||
edge_strength, edge_density = calculate_edge_strength_density(green_gray)
|
||||
local_contrast = calculate_local_contrast(green_gray)
|
||||
print(f"绿色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}")
|
||||
edge_strength, edge_density = calculate_edge_strength_density(blue_gray)
|
||||
local_contrast = calculate_local_contrast(blue_gray)
|
||||
print(f"蓝色灰度: 边缘强度: {edge_strength}, 边缘密度: {edge_density}, 局部对比度: {local_contrast}")
|
||||
|
||||
plt.subplot(2, 2, 1)
|
||||
plt.imshow(default_gray, cmap='gray')
|
||||
plt.subplot(2, 2, 2)
|
||||
plt.imshow(red_gray, cmap='gray')
|
||||
plt.subplot(2, 2, 3)
|
||||
plt.imshow(green_gray, cmap='gray')
|
||||
plt.subplot(2, 2, 4)
|
||||
plt.imshow(blue_gray, cmap='gray')
|
||||
plt.show()
|
||||
|
Before Width: | Height: | Size: 736 KiB |
@@ -7,17 +7,17 @@ from importlib.machinery import EXTENSION_SUFFIXES
|
||||
|
||||
INCLUDE_LAYOUT = {
|
||||
"configs": {
|
||||
"source_dir": Path("src/orisys/configs"),
|
||||
"source_dir": Path("config"),
|
||||
"target_subdir": Path("configs"),
|
||||
"patterns": ["ddjx01.json"],
|
||||
"patterns": ["*.json"],
|
||||
},
|
||||
"examples": {
|
||||
"source_dir": Path("."),
|
||||
"source_dir": Path("examples"),
|
||||
"target_subdir": Path("examples"),
|
||||
"files": [
|
||||
Path("example1_basic_pipeline.py"),
|
||||
Path("example2_two_sensor_pipeline.py"),
|
||||
Path("example3_plot_data.py"),
|
||||
Path("examples/basic_pipeline.py"),
|
||||
Path("examples/two_sensor_pipeline.py"),
|
||||
Path("examples/plot_data.py"),
|
||||
],
|
||||
},
|
||||
"docs": {
|
||||
@@ -774,7 +774,7 @@ def main():
|
||||
|
||||
if sensor1_active:
|
||||
img1_valid = sensor1.get_img()
|
||||
if not img1_valid:
|
||||
if img1_valid is None:
|
||||
panel1_ui["arrow_label"].hide()
|
||||
panel1_ui["plot_widget"].hide()
|
||||
panel1_ui["disconnected_label"].show()
|
||||
@@ -831,7 +831,7 @@ def main():
|
||||
|
||||
if sensor2_active:
|
||||
img2_valid = sensor2.get_img()
|
||||
if not img2_valid:
|
||||
if img2_valid is None:
|
||||
panel2_ui["arrow_label"].hide()
|
||||
panel2_ui["plot_widget"].hide()
|
||||
panel2_ui["disconnected_label"].show()
|
||||
@@ -153,7 +153,9 @@ def main():
|
||||
frame_count = 0
|
||||
writer = None
|
||||
while True:
|
||||
sensor1.get_img() # 提取并拼接复眼图像
|
||||
isimg = sensor1.get_img() # 提取并拼接复眼图像
|
||||
if isimg is None:
|
||||
break
|
||||
|
||||
|
||||
sensor1.compute_deformation() # 计算形变和分解
|
||||
@@ -165,8 +167,13 @@ def main():
|
||||
fps, fn, fx, fy, flow, depth_map = sensor1.read_info(sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW, sensor1.info.DEPTH)
|
||||
print(fps,fn,fx,fy)
|
||||
# 显示向量场
|
||||
black_img = np.zeros_like(sensor1.img)
|
||||
arrows = orisys.util.draw_arrows(sensor1.img, flow, threshold=2, grid_spacing=20, arrow_scale=0.5)
|
||||
_bg_bgr = (22, 16, 11)
|
||||
black_img = np.empty_like(sensor1.img)
|
||||
black_img[:, :, 0] = _bg_bgr[0]
|
||||
black_img[:, :, 1] = _bg_bgr[1]
|
||||
black_img[:, :, 2] = _bg_bgr[2]
|
||||
|
||||
arrows = orisys.util.draw_arrows(black_img, flow, threshold=2, grid_spacing=20, arrow_scale=0.5,below_threshold_color=(160, 160, 160),edge_margin=15,colormap=cv2.COLORMAP_PARULA)
|
||||
cv2.imshow("Deformation", arrows)
|
||||
|
||||
# 显示深度图
|
||||
@@ -0,0 +1,159 @@
|
||||
import argparse
|
||||
import av
|
||||
import cv2
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
TARGET_FPS = 30
|
||||
WIDTH = 1920
|
||||
HEIGHT = 1080
|
||||
|
||||
|
||||
def list_windows_dshow_devices():
|
||||
"""
|
||||
Print DirectShow devices using ffmpeg, if available.
|
||||
"""
|
||||
cmd = ["ffmpeg", "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]
|
||||
print("Listing Windows DirectShow devices via ffmpeg...")
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
except FileNotFoundError as exc:
|
||||
raise RuntimeError("ffmpeg was not found in PATH; cannot list DirectShow devices.") from exc
|
||||
|
||||
output = (result.stderr or "") + (result.stdout or "")
|
||||
print(output.strip())
|
||||
|
||||
|
||||
def open_pyav_capture(system_name, device, pixel_format):
|
||||
"""
|
||||
Open a camera device via PyAV on Linux or Windows.
|
||||
"""
|
||||
if system_name == "linux":
|
||||
options = {
|
||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
||||
"framerate": str(TARGET_FPS),
|
||||
"input_format": pixel_format or "mjpeg",
|
||||
}
|
||||
container = av.open(device or "/dev/video4", format="v4l2", options=options)
|
||||
elif system_name == "windows":
|
||||
if not device:
|
||||
raise RuntimeError(
|
||||
"On Windows, PyAV/dshow requires a camera name, for example "
|
||||
'--device "USB Camera". Use --list-devices first.'
|
||||
)
|
||||
options = {
|
||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
||||
"framerate": str(TARGET_FPS),
|
||||
}
|
||||
if pixel_format:
|
||||
options["pixel_format"] = pixel_format
|
||||
container = av.open(f"video={device}", format="dshow", options=options)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Unsupported OS: {platform.system()}. Only Linux and Windows are supported."
|
||||
)
|
||||
|
||||
stream = container.streams.video[0]
|
||||
return container, stream
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Capture from camera via PyAV on Linux or Windows, optionally save to MP4."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--device",
|
||||
type=str,
|
||||
default="",
|
||||
help=(
|
||||
"Linux: V4L2 device path like /dev/video4. "
|
||||
"Windows: DirectShow camera name like 'USB Camera'. "
|
||||
"If empty, Linux uses /dev/video4; Windows requires --device."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pixel-format",
|
||||
type=str,
|
||||
default="",
|
||||
help=(
|
||||
"Optional input pixel format / format hint. "
|
||||
"Examples: Linux 'mjpeg'; Windows may accept values such as 'mjpeg' if supported."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-devices",
|
||||
action="store_true",
|
||||
help="On Windows, list DirectShow device names via ffmpeg and exit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
default="",
|
||||
help="Output MP4 path (e.g. capture.mp4). If empty, only preview, no file.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
system_name = platform.system().lower()
|
||||
if args.list_devices:
|
||||
if system_name != "windows":
|
||||
raise RuntimeError("--list-devices is currently only implemented for Windows.")
|
||||
list_windows_dshow_devices()
|
||||
return
|
||||
|
||||
container, stream = open_pyav_capture(system_name, args.device, args.pixel_format)
|
||||
print(
|
||||
f"Using PyAV backend on {platform.system()} "
|
||||
f"with device={args.device or '/dev/video4'}"
|
||||
)
|
||||
|
||||
frame_count = 0
|
||||
t_start = time.perf_counter()
|
||||
|
||||
writer = None
|
||||
if args.output:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
||||
writer = cv2.VideoWriter(
|
||||
args.output, fourcc, float(TARGET_FPS), (WIDTH, HEIGHT)
|
||||
)
|
||||
if not writer.isOpened():
|
||||
container.close()
|
||||
raise RuntimeError(f"Could not open VideoWriter for {args.output}")
|
||||
print(f"Recording to {args.output} @ {TARGET_FPS} fps, {WIDTH}x{HEIGHT}")
|
||||
|
||||
try:
|
||||
for frame in container.decode(stream):
|
||||
t0 = time.perf_counter()
|
||||
img = frame.to_ndarray(format="bgr24") # OpenCV-compatible
|
||||
t1 = time.perf_counter()
|
||||
|
||||
if writer is not None:
|
||||
writer.write(img)
|
||||
|
||||
cv2.imshow("pyav_cam", img)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
t2 = time.perf_counter()
|
||||
|
||||
frame_count += 1
|
||||
if frame_count % 30 == 0:
|
||||
elapsed = t2 - t_start
|
||||
print(
|
||||
f"Frame {frame_count}: convert={1000*(t1-t0):.1f}ms, "
|
||||
f"imshow+waitKey={1000*(t2-t1):.1f}ms, avg_fps={frame_count/elapsed:.1f}"
|
||||
)
|
||||
|
||||
if key == ord("q"):
|
||||
break
|
||||
finally:
|
||||
container.close()
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
print(f"Saved {frame_count} frames to {args.output}")
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,11 +1,12 @@
|
||||
from setuptools import setup, find_packages
|
||||
from pathlib import Path
|
||||
|
||||
# Find all JSON files in the configs directory
|
||||
configs_dir = Path("src/orisys/configs")
|
||||
json_files = []
|
||||
if configs_dir.exists():
|
||||
json_files = [str(p.relative_to("src")) for p in configs_dir.glob("*.json")]
|
||||
config_dir = Path("config")
|
||||
data_files = []
|
||||
if config_dir.exists():
|
||||
json_files = [str(p) for p in sorted(config_dir.glob("*.json"))]
|
||||
if json_files:
|
||||
data_files.append(("orisys/configs", json_files))
|
||||
|
||||
setup(
|
||||
name="orisys",
|
||||
@@ -14,10 +15,8 @@ setup(
|
||||
author="Your Name",
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages(where="src"),
|
||||
package_data={
|
||||
"orisys": ["configs/*.json"],
|
||||
},
|
||||
include_package_data=True,
|
||||
data_files=data_files,
|
||||
install_requires=[
|
||||
"numpy>=1.24.4",
|
||||
"opencv-python>=4.10.0.84",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
Compute optical flow and optionally decompose it into force components.
|
||||
|
||||
Returns:
|
||||
np.ndarray: Optical flow field
|
||||
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:
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
|
||||
"""
|
||||
Prototype tactile 2.5D motion analysis on top of the existing dense flow output.
|
||||
|
||||
This script compares several motion representations derived from optical flow:
|
||||
1. Raw flow arrows / magnitude
|
||||
2. Radial and tangential flow around an estimated contact center
|
||||
3. Differential flow features: divergence, curl, strain magnitude
|
||||
4. Pseudo-height reconstructed from divergence (qualitative only)
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
WINDOW_TITLE = "Tactile 2.5D Motion Prototype"
|
||||
|
||||
_local_src = os.path.join(os.path.dirname(__file__), "src")
|
||||
if _local_src not in sys.path:
|
||||
sys.path.insert(0, _local_src)
|
||||
|
||||
import orisys
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video", "-v", type=str, default="0", help="Camera ID / video file path")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print detailed sensor logs")
|
||||
parser.add_argument("--no-display", action="store_true", help="Disable OpenCV windows")
|
||||
parser.add_argument("--save", type=str, default="", help="Optional output video path for the comparison panel")
|
||||
parser.add_argument("--max-frames", type=int, default=0, help="Stop after N frames (0 = unlimited)")
|
||||
parser.add_argument("--grid-spacing", type=int, default=20, help="Arrow visualization grid spacing")
|
||||
parser.add_argument("--flow-threshold", type=float, default=2.0, help="Arrow visualization magnitude threshold")
|
||||
parser.add_argument("--print-every", type=int, default=1, help="Print scalar metrics every N frames")
|
||||
parser.add_argument("--panel-size", type=int, default=320, help="Width/height of each panel tile")
|
||||
parser.add_argument("--plane-width-mm", type=float, default=20.0, help="Physical tactile imaging plane width in mm")
|
||||
parser.add_argument("--plane-height-mm", type=float, default=20.0, help="Physical tactile imaging plane height in mm")
|
||||
parser.add_argument("--camera-distance-mm", type=float, default=10.0, help="Distance from camera plane to imaging plane in mm")
|
||||
parser.add_argument("--display-max-width", type=int, default=1800, help="Maximum preview window width in pixels")
|
||||
parser.add_argument("--display-max-height", type=int, default=1000, help="Maximum preview window height in pixels")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def create_sensor(args):
|
||||
try:
|
||||
video_input = int(args.video)
|
||||
except ValueError:
|
||||
video_input = args.video
|
||||
|
||||
return orisys.Sensor(
|
||||
video_input,
|
||||
config_name=args.config,
|
||||
cal_path=args.cal,
|
||||
verbose=args.verbose,
|
||||
)
|
||||
|
||||
|
||||
def ensure_bgr(image):
|
||||
if image.ndim == 2:
|
||||
return cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
||||
return image
|
||||
|
||||
|
||||
def resize_panel(image, panel_size):
|
||||
return cv2.resize(ensure_bgr(image), (panel_size, panel_size), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
|
||||
def fit_display_image(image, max_width, max_height):
|
||||
h, w = image.shape[:2]
|
||||
scale = min(max_width / max(float(w), 1.0), max_height / max(float(h), 1.0), 1.0)
|
||||
if scale >= 0.999:
|
||||
return image
|
||||
new_size = (max(1, int(w * scale)), max(1, int(h * scale)))
|
||||
return cv2.resize(image, new_size, interpolation=cv2.INTER_AREA)
|
||||
|
||||
|
||||
def signed_heatmap(field, colormap=cv2.COLORMAP_TURBO):
|
||||
max_abs = float(np.max(np.abs(field)))
|
||||
if max_abs <= 1e-9:
|
||||
norm = np.full(field.shape, 127, dtype=np.uint8)
|
||||
else:
|
||||
norm = np.clip((field / max_abs + 1.0) * 127.5, 0, 255).astype(np.uint8)
|
||||
return cv2.applyColorMap(norm, colormap)
|
||||
|
||||
|
||||
def magnitude_heatmap(field, colormap=cv2.COLORMAP_TURBO):
|
||||
max_val = float(np.max(field))
|
||||
if max_val <= 1e-9:
|
||||
norm = np.zeros(field.shape, dtype=np.uint8)
|
||||
else:
|
||||
norm = np.clip(field / max_val * 255.0, 0, 255).astype(np.uint8)
|
||||
return cv2.applyColorMap(norm, colormap)
|
||||
|
||||
|
||||
def add_title(image, title):
|
||||
panel = image.copy()
|
||||
cv2.rectangle(panel, (0, 0), (panel.shape[1], 28), (0, 0, 0), -1)
|
||||
cv2.putText(panel, title, (8, 20), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (255, 255, 255), 2, cv2.LINE_AA)
|
||||
return panel
|
||||
|
||||
|
||||
def make_panel(image, title, panel_size):
|
||||
return add_title(resize_panel(image, panel_size), title)
|
||||
|
||||
|
||||
def stack_panels(panels, columns=4):
|
||||
filler = np.zeros_like(panels[0])
|
||||
rows = []
|
||||
for idx in range(0, len(panels), columns):
|
||||
row = panels[idx:idx + columns]
|
||||
if len(row) < columns:
|
||||
row = row + [filler] * (columns - len(row))
|
||||
rows.append(cv2.hconcat(row))
|
||||
return cv2.vconcat(rows)
|
||||
|
||||
|
||||
def estimate_contact_center(divergence, flow_magnitude):
|
||||
positive = np.maximum(divergence, 0.0)
|
||||
weights = positive * (flow_magnitude + 1e-6)
|
||||
total = float(weights.sum())
|
||||
h, w = divergence.shape
|
||||
|
||||
if total <= 1e-9:
|
||||
return (w // 2, h // 2)
|
||||
|
||||
ys, xs = np.indices(divergence.shape, dtype=np.float32)
|
||||
cx = int(np.clip((weights * xs).sum() / total, 0, w - 1))
|
||||
cy = int(np.clip((weights * ys).sum() / total, 0, h - 1))
|
||||
return (cx, cy)
|
||||
|
||||
|
||||
def prepare_support_mask(support_map):
|
||||
if support_map is None:
|
||||
return None
|
||||
|
||||
support = np.abs(support_map).astype(np.float32)
|
||||
max_support = float(np.max(support))
|
||||
if max_support > 0.0:
|
||||
support /= max_support
|
||||
support = (support > 0.05).astype(np.float32)
|
||||
if float(np.mean(support)) < 0.002:
|
||||
return None
|
||||
return support
|
||||
|
||||
|
||||
def decompose_radial_tangential(flow, center):
|
||||
h, w = flow.shape[:2]
|
||||
ys, xs = np.indices((h, w), dtype=np.float32)
|
||||
cx, cy = center
|
||||
|
||||
rx = xs - float(cx)
|
||||
ry = ys - float(cy)
|
||||
r_norm = np.sqrt(rx * rx + ry * ry)
|
||||
r_norm = np.where(r_norm < 1e-6, 1.0, r_norm)
|
||||
|
||||
rhat_x = rx / r_norm
|
||||
rhat_y = ry / r_norm
|
||||
that_x = -rhat_y
|
||||
that_y = rhat_x
|
||||
|
||||
u = flow[:, :, 0]
|
||||
v = flow[:, :, 1]
|
||||
radial = u * rhat_x + v * rhat_y
|
||||
tangential = u * that_x + v * that_y
|
||||
return radial, tangential
|
||||
|
||||
|
||||
def compute_flow_jacobian_features(flow):
|
||||
u = flow[:, :, 0]
|
||||
v = flow[:, :, 1]
|
||||
|
||||
du_dy, du_dx = np.gradient(u)
|
||||
dv_dy, dv_dx = np.gradient(v)
|
||||
|
||||
divergence = du_dx + dv_dy
|
||||
curl = dv_dx - du_dy
|
||||
strain_xx = du_dx
|
||||
strain_yy = dv_dy
|
||||
strain_xy = 0.5 * (du_dy + dv_dx)
|
||||
strain_mag = np.sqrt(strain_xx * strain_xx + strain_yy * strain_yy + 2.0 * strain_xy * strain_xy)
|
||||
|
||||
return {
|
||||
"divergence": divergence,
|
||||
"curl": curl,
|
||||
"strain_mag": strain_mag,
|
||||
}
|
||||
|
||||
|
||||
def reconstruct_pseudo_height(divergence, flow_magnitude=None, support_map=None):
|
||||
rhs = divergence.astype(np.float32).copy()
|
||||
|
||||
# No-contact guard: tiny flow noise becomes large low-frequency drift after Poisson solve.
|
||||
if flow_magnitude is not None:
|
||||
mean_mag = float(np.mean(flow_magnitude))
|
||||
max_mag = float(np.max(flow_magnitude))
|
||||
if mean_mag < 0.03 and max_mag < 0.15:
|
||||
return np.zeros_like(rhs, dtype=np.float32)
|
||||
|
||||
support = prepare_support_mask(support_map)
|
||||
if support_map is not None:
|
||||
if support is None:
|
||||
return np.zeros_like(rhs, dtype=np.float32)
|
||||
rhs *= support
|
||||
|
||||
noise_floor = max(float(np.std(rhs)) * 1.5, 1e-4)
|
||||
rhs[np.abs(rhs) < noise_floor] = 0.0
|
||||
if not np.any(rhs):
|
||||
return np.zeros_like(rhs, dtype=np.float32)
|
||||
|
||||
# Zero-pad before FFT solve to reduce periodic wrap-around ghosts, then crop back.
|
||||
h, w = rhs.shape
|
||||
pad_y = max(8, h // 2)
|
||||
pad_x = max(8, w // 2)
|
||||
rhs_padded = np.pad(rhs, ((pad_y, pad_y), (pad_x, pad_x)), mode="constant")
|
||||
rhs_padded = rhs_padded - float(rhs_padded.mean())
|
||||
|
||||
hp, wp = rhs_padded.shape
|
||||
fy = np.fft.fftfreq(hp).reshape(-1, 1)
|
||||
fx = np.fft.fftfreq(wp).reshape(1, -1)
|
||||
denom = (2.0 * np.pi * fx) ** 2 + (2.0 * np.pi * fy) ** 2
|
||||
denom[0, 0] = 1.0
|
||||
|
||||
rhs_hat = np.fft.fft2(rhs_padded)
|
||||
height_hat = -rhs_hat / denom
|
||||
height_hat[0, 0] = 0.0
|
||||
height_full = np.real(np.fft.ifft2(height_hat)).astype(np.float32)
|
||||
height = height_full[pad_y:pad_y + h, pad_x:pad_x + w]
|
||||
height = cv2.GaussianBlur(height, (0, 0), 1.0)
|
||||
return height
|
||||
|
||||
|
||||
def build_visualization(img, flow, center, features, radial, tangential, pseudo_height, depth_map, args):
|
||||
arrows = orisys.util.draw_arrows(
|
||||
img,
|
||||
flow,
|
||||
threshold=args.flow_threshold,
|
||||
grid_spacing=args.grid_spacing,
|
||||
arrow_scale=1.0,
|
||||
)
|
||||
|
||||
contact_view = ensure_bgr(img)
|
||||
cv2.circle(contact_view, center, 6, (0, 255, 0), -1)
|
||||
cv2.circle(contact_view, center, 12, (0, 255, 0), 2)
|
||||
|
||||
panel_images = [
|
||||
(contact_view, "Image / center"),
|
||||
(arrows, "Raw flow"),
|
||||
(signed_heatmap(radial), "Radial flow"),
|
||||
(signed_heatmap(tangential), "Tangential flow"),
|
||||
(signed_heatmap(features["divergence"]), "Divergence"),
|
||||
(signed_heatmap(features["curl"]), "Curl"),
|
||||
(magnitude_heatmap(features["strain_mag"]), "Strain mag"),
|
||||
(signed_heatmap(depth_map), "Depth map"),
|
||||
(signed_heatmap(pseudo_height), "Pseudo height"),
|
||||
]
|
||||
panels = [make_panel(image, title, args.panel_size) for image, title in panel_images]
|
||||
return stack_panels(panels)
|
||||
|
||||
|
||||
def make_writer(path, frame_size, fps):
|
||||
if not path:
|
||||
return None
|
||||
parent = Path(path).parent
|
||||
if parent and not parent.exists():
|
||||
parent.mkdir(parents=True, exist_ok=True)
|
||||
return cv2.VideoWriter(path, cv2.VideoWriter_fourcc(*"mp4v"), fps, frame_size)
|
||||
|
||||
|
||||
def approximate_raw_flow_to_mm(flow_px, plane_width_mm, plane_height_mm, camera_distance_mm, image_shape):
|
||||
h, w = image_shape[:2]
|
||||
fx = camera_distance_mm * w / max(plane_width_mm, 1e-6)
|
||||
fy = camera_distance_mm * h / max(plane_height_mm, 1e-6)
|
||||
flow_mm = flow_px.copy().astype(np.float32)
|
||||
flow_mm[:, :, 0] *= camera_distance_mm / max(fx, 1e-6)
|
||||
flow_mm[:, :, 1] *= camera_distance_mm / max(fy, 1e-6)
|
||||
return flow_mm
|
||||
|
||||
|
||||
def compute_metrics(flow_magnitude, raw_flow_mm, radial, tangential, features, depth_map, pseudo_height):
|
||||
return {
|
||||
"flow_mean": float(flow_magnitude.mean()),
|
||||
"flow_mean_mm": float(np.linalg.norm(raw_flow_mm, axis=2).mean()),
|
||||
"radial_abs_mean": float(np.mean(np.abs(radial))),
|
||||
"tangential_abs_mean": float(np.mean(np.abs(tangential))),
|
||||
"divergence_mean": float(features["divergence"].mean()),
|
||||
"curl_abs_mean": float(np.mean(np.abs(features["curl"]))),
|
||||
"strain_mean": float(features["strain_mag"].mean()),
|
||||
"depth_peak": float(np.max(np.abs(depth_map))),
|
||||
"pseudo_height_peak": float(np.max(np.abs(pseudo_height))),
|
||||
}
|
||||
|
||||
|
||||
def format_metrics(frame_count, fps, metrics):
|
||||
line = (
|
||||
f"frame={frame_count:04d} fps={fps:6.2f} "
|
||||
f"flow_mean={metrics['flow_mean']:.4f} "
|
||||
f"flow_mean_mm={metrics['flow_mean_mm']:.4f} "
|
||||
f"radial_abs={metrics['radial_abs_mean']:.4f} "
|
||||
f"tangential_abs={metrics['tangential_abs_mean']:.4f} "
|
||||
f"div_mean={metrics['divergence_mean']:.4f} "
|
||||
f"curl_abs={metrics['curl_abs_mean']:.4f} "
|
||||
f"strain={metrics['strain_mean']:.4f} "
|
||||
f"depth_peak={metrics['depth_peak']:.4f} "
|
||||
f"height_peak={metrics['pseudo_height_peak']:.4f}"
|
||||
)
|
||||
t = metrics.get("_timings")
|
||||
if t:
|
||||
line += (
|
||||
f" | analyze {t['total']:.2f}ms"
|
||||
f" (mag:{t['magnitude']:.2f} jac:{t['jacobian']:.2f}"
|
||||
f" rad_tan:{t['rad_tan']:.2f} pseudo_h:{t['pseudo_h']:.2f}"
|
||||
f" metrics:{t['metrics']:.2f} vis:{t['visualize']:.2f})"
|
||||
)
|
||||
return line
|
||||
|
||||
|
||||
def read_sensor_frame(sensor):
|
||||
sensor.compute_flow()
|
||||
sensor.compute_hhd()
|
||||
sensor.compute_contact()
|
||||
return sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.IMG,
|
||||
sensor.info.DEPTH,
|
||||
)
|
||||
|
||||
|
||||
def analyze_frame(img, flow, depth_map, args):
|
||||
t = time.perf_counter()
|
||||
|
||||
flow_magnitude = np.linalg.norm(flow, axis=2)
|
||||
t_mag = time.perf_counter()
|
||||
|
||||
features = compute_flow_jacobian_features(flow)
|
||||
t_jac = time.perf_counter()
|
||||
|
||||
center = estimate_contact_center(features["divergence"], flow_magnitude)
|
||||
radial, tangential = decompose_radial_tangential(flow, center)
|
||||
t_decomp = time.perf_counter()
|
||||
|
||||
pseudo_height = reconstruct_pseudo_height(
|
||||
features["divergence"],
|
||||
flow_magnitude=flow_magnitude,
|
||||
support_map=depth_map,
|
||||
)
|
||||
t_height = time.perf_counter()
|
||||
|
||||
raw_flow_mm = approximate_raw_flow_to_mm(
|
||||
flow,
|
||||
args.plane_width_mm,
|
||||
args.plane_height_mm,
|
||||
args.camera_distance_mm,
|
||||
img.shape,
|
||||
)
|
||||
metrics = compute_metrics(flow_magnitude, raw_flow_mm, radial, tangential, features, depth_map, pseudo_height)
|
||||
t_metrics = time.perf_counter()
|
||||
|
||||
canvas = build_visualization(
|
||||
img,
|
||||
flow,
|
||||
center,
|
||||
features,
|
||||
radial,
|
||||
tangential,
|
||||
pseudo_height,
|
||||
depth_map,
|
||||
args,
|
||||
)
|
||||
t_vis = time.perf_counter()
|
||||
|
||||
metrics["_timings"] = {
|
||||
"magnitude": (t_mag - t) * 1000,
|
||||
"jacobian": (t_jac - t_mag) * 1000,
|
||||
"rad_tan": (t_decomp - t_jac) * 1000,
|
||||
"pseudo_h": (t_height - t_decomp) * 1000,
|
||||
"metrics": (t_metrics - t_height) * 1000,
|
||||
"visualize": (t_vis - t_metrics) * 1000,
|
||||
"total": (t_vis - t) * 1000,
|
||||
}
|
||||
return canvas, metrics
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
sensor = create_sensor(args)
|
||||
writer = None
|
||||
frame_count = 0
|
||||
print_every = max(args.print_every, 1)
|
||||
|
||||
try:
|
||||
print("\nPress 'q' to quit, 'r' to reset tracking.")
|
||||
|
||||
while True:
|
||||
img = sensor.get_img()
|
||||
if img is None:
|
||||
print("No more frames returned from sensor.")
|
||||
break
|
||||
|
||||
fps, flow, img, depth_map = read_sensor_frame(sensor)
|
||||
canvas, metrics = analyze_frame(img, flow, depth_map, args)
|
||||
|
||||
if writer is None and args.save:
|
||||
writer = make_writer(args.save, (canvas.shape[1], canvas.shape[0]), max(float(fps), 1.0))
|
||||
|
||||
if writer is not None:
|
||||
writer.write(canvas)
|
||||
|
||||
frame_count += 1
|
||||
|
||||
if frame_count % print_every == 0:
|
||||
print(format_metrics(frame_count, fps, metrics))
|
||||
|
||||
if not args.no_display:
|
||||
display_canvas = fit_display_image(
|
||||
canvas,
|
||||
max_width=args.display_max_width,
|
||||
max_height=args.display_max_height,
|
||||
)
|
||||
cv2.imshow(WINDOW_TITLE, display_canvas)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\nExiting...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
sensor.reset()
|
||||
print("Sensor reset.")
|
||||
|
||||
if args.max_frames > 0 and frame_count >= args.max_frames:
|
||||
print(f"Reached max_frames={args.max_frames}.")
|
||||
break
|
||||
finally:
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
sensor.disconnect()
|
||||
if not args.no_display:
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,207 +0,0 @@
|
||||
"""
|
||||
示例: 密度图可视化
|
||||
|
||||
本示例演示如何:
|
||||
1. 获取拼接图像
|
||||
2. 计算光流
|
||||
3. 基于光流生成 density map
|
||||
4. 使用 colormap 实时显示
|
||||
"""
|
||||
import os
|
||||
import argparse
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import orisys
|
||||
|
||||
from getDensity import getFilteredDensity
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
|
||||
def density_to_colormap(density):
|
||||
"""Convert density map to a displayable heatmap."""
|
||||
if density is None or density.size == 0:
|
||||
return np.zeros((1, 1, 3), dtype=np.uint8)
|
||||
|
||||
dmin = float(np.min(density))
|
||||
dmax = float(np.max(density))
|
||||
if dmax > dmin:
|
||||
density_u8 = ((density - dmin) * (255.0 / (dmax - dmin))).astype(np.uint8)
|
||||
else:
|
||||
density_u8 = np.zeros_like(density, dtype=np.uint8)
|
||||
return cv2.applyColorMap(density_u8, cv2.COLORMAP_TURBO)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video", "-v", type=str, default="0", help="Camera ID / Video file path")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="Print detailed information")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 步骤1:创建传感器对象 (必要步骤)
|
||||
try:
|
||||
vid_src = int(args.video)
|
||||
except ValueError:
|
||||
vid_src = args.video
|
||||
sensor = orisys.Sensor(vid_src, config_name=args.config, cal_path=args.cal, verbose=args.verbose)
|
||||
|
||||
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
||||
|
||||
while True:
|
||||
# 步骤2:获取图像并计算形变
|
||||
img = sensor.get_img()
|
||||
if img is None:
|
||||
print("Failed to get image (end of stream or capture error). Exiting.")
|
||||
break
|
||||
|
||||
sensor.compute_deformation(check_motion=False)
|
||||
sensor.compute_contact()
|
||||
(fps, flow, contour, centroid) = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.CONTOUR,
|
||||
sensor.info.CENTROID,
|
||||
)
|
||||
|
||||
# 步骤3:根据光流计算 density map 并显示
|
||||
density = getFilteredDensity(flow, use_cuda=sensor.cuda)
|
||||
density_view = density_to_colormap(density)
|
||||
cv2.imshow("Density Map", density_view)
|
||||
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("Contact Centroid", image_with_foe)
|
||||
|
||||
print(f"fps: {fps:.2f}, density_shape: {density.shape}, flow_shape: {flow.shape}")
|
||||
|
||||
# 步骤4:键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
sensor.disconnect()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
"""
|
||||
示例 1: 基础用法 - 单传感器同步处理
|
||||
|
||||
本示例演示 orisys 的基本使用流程:
|
||||
1. 初始化传感器(视频文件或摄像头)
|
||||
2. 循环获取图像并计算形变和触碰点
|
||||
3. 读取各类传感器数据
|
||||
4. 可视化显示
|
||||
|
||||
|
||||
"""
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
|
||||
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 步骤1:创建传感器对象 (必要步骤),
|
||||
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
||||
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
||||
|
||||
# 方式1: 从视频文件读取
|
||||
try:
|
||||
input = int(args.video)
|
||||
is_camera = True
|
||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
except:
|
||||
is_camera = False
|
||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
|
||||
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
||||
|
||||
while True:
|
||||
# 步骤2:信息提取
|
||||
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
||||
sensor.get_img()
|
||||
# 步骤 2.2: 计算形变场 (必要步骤)
|
||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
|
||||
# 步骤 2.3: 计算触碰点
|
||||
if is_touched:
|
||||
sensor.compute_contact()
|
||||
|
||||
|
||||
#步骤 2.4: 读取信息
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.VNORMAL,
|
||||
sensor.info.IMG,
|
||||
sensor.info.CONTOUR,
|
||||
sensor.info.CENTROID,
|
||||
sensor.info.DEPTH,
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY
|
||||
)
|
||||
|
||||
#步骤 3: 可视化显示
|
||||
# 步骤 3.1: 绘制形变场
|
||||
arrows = orisys.util.draw_arrows(
|
||||
img,
|
||||
flow,
|
||||
threshold=2,
|
||||
grid_spacing=20,
|
||||
arrow_scale=1.0
|
||||
)
|
||||
cv2.imshow("Deformation", arrows)
|
||||
|
||||
# 步骤 3.2: 在图像上触碰点和区域
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("Contact Centroid", image_with_foe)
|
||||
|
||||
# 步骤 3.3: 绘制深度图
|
||||
# 归一化散度值到0-255范围(使用绝对值,然后归一化)
|
||||
div_abs = depth_map
|
||||
if div_abs.max() > div_abs.min():
|
||||
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
||||
else:
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
|
||||
|
||||
#步骤 3.4: 打印资料
|
||||
print(f"fps: {fps}, fnormal: {fnormal}, fshearx: {fshearx}, fsheary: {fsheary}, depth_shape: {depth_map.shape}, sensor.resolution_raw: {sensor.img_size_raw}")
|
||||
|
||||
#步骤 3.5: 键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
# =========================================================================
|
||||
# 4. 清理资源
|
||||
# =========================================================================
|
||||
sensor.disconnect() # 释放视频源和相关资源
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,252 +0,0 @@
|
||||
"""
|
||||
HHD Downsample Factor Comparison
|
||||
|
||||
Reads a video file and runs the full pipeline at multiple hhd_downsample factors.
|
||||
Computes per-frame HHD timing and vnormal quality metrics against the baseline (factor=1),
|
||||
then plots the results.
|
||||
|
||||
Usage:
|
||||
python test_downsample.py -v jx01_hex1.5_2.mp4 -c ./config/prototype_of3_720p.json -f 1,2,3,4 -n 50 -w 30
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
_local_src = os.path.join(os.path.dirname(__file__), "src")
|
||||
if _local_src not in sys.path:
|
||||
sys.path.insert(0, _local_src)
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
|
||||
def run_sensor(vid_src, config, cal, downsample, n_warmup, n_measure):
|
||||
"""Run sensor for n_warmup + n_measure frames. Capture timing and vnormal for every measurement frame."""
|
||||
try:
|
||||
vid = int(vid_src)
|
||||
except ValueError:
|
||||
vid = vid_src
|
||||
|
||||
sensor = orisys.Sensor(
|
||||
vid, config_name=config, cal_path=cal,
|
||||
isstitch=True, cuda=False, verbose=False,
|
||||
hhd_downsample=downsample,
|
||||
)
|
||||
|
||||
hhd_times = []
|
||||
vnormals = []
|
||||
frame_idx = 0
|
||||
n_total = n_warmup + n_measure
|
||||
|
||||
while frame_idx < n_total:
|
||||
try:
|
||||
sensor.get_img()
|
||||
except Exception:
|
||||
break
|
||||
|
||||
sensor.compute_flow()
|
||||
|
||||
t0 = time.perf_counter()
|
||||
sensor.compute_hhd()
|
||||
t_hhd = time.perf_counter() - t0
|
||||
|
||||
if frame_idx >= n_warmup:
|
||||
hhd_times.append(t_hhd)
|
||||
vn = sensor.vnormal.copy() if sensor.vnormal is not None else None
|
||||
vnormals.append(vn)
|
||||
|
||||
frame_idx += 1
|
||||
|
||||
sensor.disconnect()
|
||||
return {"hhd_times": hhd_times, "vnormals": vnormals}
|
||||
|
||||
|
||||
def per_frame_rmse(a, b):
|
||||
if a is None or b is None:
|
||||
return float("nan")
|
||||
diff = a.astype(np.float64) - b.astype(np.float64)
|
||||
return float(np.sqrt(np.mean(diff ** 2)))
|
||||
|
||||
|
||||
def per_frame_max_abs_err(a, b):
|
||||
if a is None or b is None:
|
||||
return float("nan")
|
||||
return float(np.max(np.abs(a.astype(np.float64) - b.astype(np.float64))))
|
||||
|
||||
|
||||
def per_frame_cosine_sim(a, b):
|
||||
if a is None or b is None:
|
||||
return float("nan")
|
||||
a64 = a.astype(np.float64)
|
||||
b64 = b.astype(np.float64)
|
||||
dot = np.sum(a64 * b64, axis=-1)
|
||||
mag_a = np.linalg.norm(a64, axis=-1)
|
||||
mag_b = np.linalg.norm(b64, axis=-1)
|
||||
denom = mag_a * mag_b
|
||||
valid = denom > 1e-12
|
||||
if not np.any(valid):
|
||||
return float("nan")
|
||||
return float(np.mean(dot[valid] / denom[valid]))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Compare HHD downsample factors")
|
||||
parser.add_argument("--video", "-v", type=str, default="2", help="Camera ID or video file path")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/prototype_of3_720p.json")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy")
|
||||
parser.add_argument("--factors", "-f", type=str, default="1,2,3", help="Comma-separated downsample factors")
|
||||
parser.add_argument("--warmup", "-w", type=int, default=5, help="Warmup frames (skipped)")
|
||||
parser.add_argument("--frames", "-n", type=int, default=30, help="Measurement frames")
|
||||
parser.add_argument("--output", "-o", type=str, default="downsample_comparison.png", help="Output plot filename")
|
||||
args = parser.parse_args()
|
||||
|
||||
factors = [int(f) for f in args.factors.split(",")]
|
||||
|
||||
print(f"Video: {args.video}")
|
||||
print(f"Config: {args.config}")
|
||||
print(f"Factors: {factors}")
|
||||
print(f"Warmup: {args.warmup} frames, Measure: {args.frames} frames")
|
||||
print()
|
||||
|
||||
# --- collect data ---
|
||||
results = {}
|
||||
for factor in factors:
|
||||
print(f"--- Running factor {factor} ---")
|
||||
results[factor] = run_sensor(
|
||||
args.video, args.config, args.cal,
|
||||
factor, args.warmup, args.frames,
|
||||
)
|
||||
n = len(results[factor]["hhd_times"])
|
||||
if n > 0:
|
||||
arr = np.array(results[factor]["hhd_times"]) * 1000
|
||||
print(f" {n} frames: hhd mean={arr.mean():.2f}ms, median={np.median(arr):.2f}ms, "
|
||||
f"min={arr.min():.2f}ms, max={arr.max():.2f}ms")
|
||||
else:
|
||||
print(" No frames captured")
|
||||
print()
|
||||
|
||||
# --- timing summary table ---
|
||||
print("=" * 72)
|
||||
print(f"{'Factor':<10} {'Mean HHD (ms)':<16} {'Median (ms)':<14} {'Speedup':<10}")
|
||||
print("-" * 72)
|
||||
baseline_median = None
|
||||
for factor in factors:
|
||||
arr = np.array(results[factor]["hhd_times"]) * 1000
|
||||
if len(arr) == 0:
|
||||
continue
|
||||
med = float(np.median(arr))
|
||||
if baseline_median is None:
|
||||
baseline_median = med
|
||||
speedup = baseline_median / med if med > 0 else 0
|
||||
print(f"{factor:<10} {arr.mean():<16.2f} {med:<14.2f} {speedup:<10.2f}x")
|
||||
print("=" * 72)
|
||||
print()
|
||||
|
||||
# --- compute per-frame quality metrics vs baseline (factor=1) ---
|
||||
has_baseline = 1 in factors and len(factors) >= 2
|
||||
metrics = {} # factor -> {"rmse": [...], "max_err": [...], "cosine": [...]}
|
||||
if has_baseline:
|
||||
base_vn = results[1]["vnormals"]
|
||||
n_base = len(base_vn)
|
||||
for factor in factors:
|
||||
if factor == 1:
|
||||
continue
|
||||
test_vn = results[factor]["vnormals"]
|
||||
n_common = min(n_base, len(test_vn))
|
||||
rmse_arr, maxerr_arr, cos_arr = [], [], []
|
||||
for i in range(n_common):
|
||||
rmse_arr.append(per_frame_rmse(base_vn[i], test_vn[i]))
|
||||
maxerr_arr.append(per_frame_max_abs_err(base_vn[i], test_vn[i]))
|
||||
cos_arr.append(per_frame_cosine_sim(base_vn[i], test_vn[i]))
|
||||
metrics[factor] = {
|
||||
"rmse": np.array(rmse_arr),
|
||||
"max_err": np.array(maxerr_arr),
|
||||
"cosine": np.array(cos_arr),
|
||||
}
|
||||
|
||||
# print per-factor summary
|
||||
print(f"{'Factor':<10} {'Mean RMSE':<14} {'Mean MaxErr':<14} {'Mean CosSim':<14}")
|
||||
print("-" * 52)
|
||||
for factor in sorted(metrics):
|
||||
m = metrics[factor]
|
||||
print(f"{factor:<10} {np.nanmean(m['rmse']):<14.6f} {np.nanmean(m['max_err']):<14.4f} "
|
||||
f"{np.nanmean(m['cosine']):<14.6f}")
|
||||
print("=" * 52)
|
||||
print()
|
||||
|
||||
# --- plot ---
|
||||
n_quality_factors = len(metrics)
|
||||
n_plots = 1 + (3 if has_baseline else 0) # timing + (rmse, max_err, cosine)
|
||||
fig, axes = plt.subplots(n_plots, 1, figsize=(12, 3.5 * n_plots), sharex=True)
|
||||
if n_plots == 1:
|
||||
axes = [axes]
|
||||
|
||||
colors = plt.cm.tab10.colors
|
||||
frame_nums = None
|
||||
|
||||
# 1) HHD timing per frame
|
||||
ax = axes[0]
|
||||
for i, factor in enumerate(factors):
|
||||
arr = np.array(results[factor]["hhd_times"]) * 1000
|
||||
frames = np.arange(len(arr))
|
||||
if frame_nums is None:
|
||||
frame_nums = frames
|
||||
ax.plot(frames, arr, label=f"ds={factor}", color=colors[i % len(colors)], linewidth=1)
|
||||
ax.set_ylabel("HHD time (ms)")
|
||||
ax.set_title("Per-frame HHD computation time")
|
||||
ax.legend(loc="upper right")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
if has_baseline:
|
||||
# 2) RMSE
|
||||
ax = axes[1]
|
||||
for i, factor in enumerate(sorted(metrics)):
|
||||
m = metrics[factor]
|
||||
frames = np.arange(len(m["rmse"]))
|
||||
ci = factors.index(factor) if factor in factors else i
|
||||
ax.plot(frames, m["rmse"], label=f"ds={factor}", color=colors[ci % len(colors)], linewidth=1)
|
||||
ax.set_ylabel("RMSE (vnormal)")
|
||||
ax.set_title("Per-frame RMSE vs baseline (ds=1)")
|
||||
ax.legend(loc="upper right")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# 3) Max absolute error
|
||||
ax = axes[2]
|
||||
for i, factor in enumerate(sorted(metrics)):
|
||||
m = metrics[factor]
|
||||
frames = np.arange(len(m["max_err"]))
|
||||
ci = factors.index(factor) if factor in factors else i
|
||||
ax.plot(frames, m["max_err"], label=f"ds={factor}", color=colors[ci % len(colors)], linewidth=1)
|
||||
ax.set_ylabel("Max Abs Error (vnormal)")
|
||||
ax.set_title("Per-frame max absolute error vs baseline (ds=1)")
|
||||
ax.legend(loc="upper right")
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
# 4) Cosine similarity
|
||||
ax = axes[3]
|
||||
for i, factor in enumerate(sorted(metrics)):
|
||||
m = metrics[factor]
|
||||
frames = np.arange(len(m["cosine"]))
|
||||
ci = factors.index(factor) if factor in factors else i
|
||||
ax.plot(frames, m["cosine"], label=f"ds={factor}", color=colors[ci % len(colors)], linewidth=1)
|
||||
ax.set_ylabel("Cosine Similarity")
|
||||
ax.set_title("Per-frame cosine similarity vs baseline (ds=1)")
|
||||
ax.legend(loc="upper right")
|
||||
ax.grid(True, alpha=0.3)
|
||||
ax.set_ylim(bottom=min(0.999, ax.get_ylim()[0]))
|
||||
|
||||
axes[-1].set_xlabel("Frame")
|
||||
fig.tight_layout()
|
||||
fig.savefig(args.output, dpi=150)
|
||||
print(f"Plot saved to: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,81 +0,0 @@
|
||||
import argparse
|
||||
import av
|
||||
import cv2
|
||||
import time
|
||||
|
||||
DEVICE = "/dev/video4"
|
||||
TARGET_FPS = 30
|
||||
WIDTH = 1920
|
||||
HEIGHT = 1080
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Capture from V4L2 via PyAV, optionally save to MP4.")
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
default="",
|
||||
help="Output MP4 path (e.g. capture.mp4). If empty, only preview, no file.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# Open v4l2 device with same style options as ffmpeg CLI
|
||||
container = av.open(
|
||||
DEVICE,
|
||||
format="v4l2",
|
||||
options={
|
||||
"input_format": "mjpeg",
|
||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
||||
"framerate": str(TARGET_FPS),
|
||||
},
|
||||
)
|
||||
|
||||
stream = container.streams.video[0]
|
||||
frame_count = 0
|
||||
t_start = time.perf_counter()
|
||||
|
||||
writer = None
|
||||
if args.output:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
||||
writer = cv2.VideoWriter(
|
||||
args.output, fourcc, float(TARGET_FPS), (WIDTH, HEIGHT)
|
||||
)
|
||||
if not writer.isOpened():
|
||||
container.close()
|
||||
raise RuntimeError(f"Could not open VideoWriter for {args.output}")
|
||||
print(f"Recording to {args.output} @ {TARGET_FPS} fps, {WIDTH}x{HEIGHT}")
|
||||
|
||||
try:
|
||||
for frame in container.decode(stream):
|
||||
t0 = time.perf_counter()
|
||||
img = frame.to_ndarray(format="bgr24") # OpenCV-compatible
|
||||
t1 = time.perf_counter()
|
||||
|
||||
if writer is not None:
|
||||
writer.write(img)
|
||||
|
||||
cv2.imshow("pyav_cam", img)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
t2 = time.perf_counter()
|
||||
|
||||
frame_count += 1
|
||||
if frame_count % 30 == 0:
|
||||
elapsed = t2 - t_start
|
||||
print(
|
||||
f"Frame {frame_count}: convert={1000*(t1-t0):.1f}ms, "
|
||||
f"imshow+waitKey={1000*(t2-t1):.1f}ms, avg_fps={frame_count/elapsed:.1f}"
|
||||
)
|
||||
|
||||
if key == ord("q"):
|
||||
break
|
||||
finally:
|
||||
container.close()
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
print(f"Saved {frame_count} frames to {args.output}")
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,121 +0,0 @@
|
||||
"""
|
||||
示例 1: 基础用法 - 单传感器同步处理
|
||||
|
||||
本示例演示 orisys 的基本使用流程:
|
||||
1. 初始化传感器(视频文件或摄像头)
|
||||
2. 循环获取图像并计算形变和触碰点
|
||||
3. 读取各类传感器数据
|
||||
4. 可视化显示
|
||||
|
||||
|
||||
"""
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import argparse
|
||||
|
||||
def flow_to_colormap(flow):
|
||||
"""Visualize optical flow with hue for direction and value for magnitude."""
|
||||
if flow is None or flow.size == 0:
|
||||
return np.zeros((1, 1, 3), dtype=np.uint8)
|
||||
|
||||
fx = flow[:, :, 0].astype(np.float32, copy=False)
|
||||
fy = flow[:, :, 1].astype(np.float32, copy=False)
|
||||
mag, ang = cv2.cartToPolar(fx, fy, angleInDegrees=True)
|
||||
|
||||
hsv = np.zeros(flow.shape[:2] + (3,), dtype=np.uint8)
|
||||
hsv[:, :, 0] = (ang / 2).astype(np.uint8)
|
||||
hsv[:, :, 1] = 255
|
||||
if mag.max() > 0:
|
||||
hsv[:, :, 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
|
||||
return cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
|
||||
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
|
||||
args = parser.parse_args()
|
||||
|
||||
# 步骤1:创建传感器对象 (必要步骤),
|
||||
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
||||
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
||||
|
||||
# 方式1: 从视频文件读取
|
||||
try:
|
||||
input = int(args.video)
|
||||
is_camera = True
|
||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
except:
|
||||
is_camera = False
|
||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
|
||||
|
||||
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
||||
|
||||
while True:
|
||||
# 步骤2:信息提取
|
||||
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
||||
sensor.get_img()
|
||||
# 步骤 2.2: 计算形变场 (必要步骤)
|
||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=6)
|
||||
# 步骤 2.3: 计算触碰点
|
||||
if is_touched:
|
||||
sensor.compute_contact()
|
||||
|
||||
|
||||
#步骤 2.4: 读取信息
|
||||
fps, flow, vnormal, img, contour, centroid, depth_map, fnormal, fshearx, fsheary = sensor.read_info(
|
||||
sensor.info.FPS,
|
||||
sensor.info.VRAW,
|
||||
sensor.info.VNORMAL,
|
||||
sensor.info.IMG,
|
||||
sensor.info.CONTOUR,
|
||||
sensor.info.CENTROID,
|
||||
sensor.info.DEPTH,
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY
|
||||
)
|
||||
|
||||
flow_colormap = flow_to_colormap(vnormal)
|
||||
cv2.imshow("Optical Flow Colormap", flow_colormap)
|
||||
|
||||
# 步骤 3.2: 在图像上触碰点和区域
|
||||
image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
cv2.imshow("Contact Centroid", image_with_foe)
|
||||
|
||||
# 步骤 3.3: 绘制深度图
|
||||
# 归一化散度值到0-255范围(使用绝对值,然后归一化)
|
||||
div_abs = depth_map
|
||||
if div_abs.max() > div_abs.min():
|
||||
div_normalized = ((div_abs - div_abs.min()) / (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
||||
else:
|
||||
div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
||||
|
||||
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
#步骤 3.4: 打印资料
|
||||
print(f"fps: {fps}, fnormal: {fnormal}, fshearx: {fshearx}, fsheary: {fsheary}, depth_shape: {depth_map.shape}, sensor.resolution_raw: {sensor.img_size_raw}")
|
||||
|
||||
#步骤 3.5: 键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
# =========================================================================
|
||||
# 4. 清理资源
|
||||
# =========================================================================
|
||||
sensor.disconnect() # 释放视频源和相关资源
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,33 +0,0 @@
|
||||
import pyopencl as cl
|
||||
|
||||
# Get all platforms
|
||||
platforms = cl.get_platforms()
|
||||
if not platforms:
|
||||
raise RuntimeError("No OpenCL platforms found")
|
||||
|
||||
# Use the first platform (rusticl in your case)
|
||||
platform = platforms[0]
|
||||
print("Using platform:", platform.name)
|
||||
|
||||
# Get devices for that platform
|
||||
devices = platform.get_devices()
|
||||
if not devices:
|
||||
raise RuntimeError("No devices found on platform")
|
||||
|
||||
for platform in cl.get_platforms():
|
||||
for device in platform.get_devices():
|
||||
print("Device:", device.name)
|
||||
# if device.type & cl.device_type.GPU == 0:
|
||||
# print("Skipping non-GPU device:", device.name)
|
||||
|
||||
# Use the first device (your Adreno GPU)
|
||||
device = devices[0]
|
||||
print("Using device:", device.name)
|
||||
|
||||
# Create a context with that device
|
||||
ctx = cl.Context([device])
|
||||
|
||||
# Create a command queue
|
||||
queue = cl.CommandQueue(ctx)
|
||||
|
||||
print("Context and command queue created successfully!")
|
||||
@@ -1,77 +0,0 @@
|
||||
"""
|
||||
示例 1: 基础用法 - 单传感器同步处理
|
||||
|
||||
本示例演示 orisys 的基本使用流程:
|
||||
1. 初始化传感器(视频文件或摄像头)
|
||||
2. 循环获取图像并计算形变和触碰点
|
||||
3. 读取各类传感器数据
|
||||
4. 可视化显示
|
||||
"""
|
||||
import os
|
||||
import argparse
|
||||
|
||||
import cv2
|
||||
import orisys
|
||||
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video", "-v", type=str, default="0", help="Camera ID / Video file path")
|
||||
parser.add_argument("--config", "-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal", "-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose", "-verbose", type=bool, default=True, help="Print detailed information")
|
||||
parser.add_argument(
|
||||
"--match-brightness",
|
||||
dest="match_brightness",
|
||||
action="store_true",
|
||||
help="Enable per-tile brightness matching",
|
||||
)
|
||||
parser.set_defaults(match_brightness=False)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
vid_src = int(args.video)
|
||||
except ValueError:
|
||||
vid_src = args.video
|
||||
|
||||
# 步骤1:创建传感器对象 (必要步骤),
|
||||
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
||||
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
||||
sensor = orisys.Sensor(vid_src, config_name=args.config, cal_path=args.cal, verbose=args.verbose)
|
||||
sensor.stitcher.configure_feature_stitch(match_brightness=args.match_brightness)
|
||||
|
||||
print("\n按 'q' 退出程序,按 'r' 重置传感器追踪器")
|
||||
|
||||
while True:
|
||||
# 步骤2:信息提取
|
||||
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
||||
img = sensor.get_img()
|
||||
if img is None:
|
||||
print("Failed to get image (end of stream or capture error). Exiting.")
|
||||
break
|
||||
|
||||
if img is not None:
|
||||
cv2.imshow("OpenCV Stitch (feature-based)", img)
|
||||
|
||||
# 步骤 2.2: 计算形变场 (必要步骤) — sensor.compute_deformation(...)
|
||||
# 步骤 2.3: 计算触碰点
|
||||
# 步骤 3: 键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
# =========================================================================
|
||||
# 4. 清理资源
|
||||
# =========================================================================
|
||||
sensor.disconnect() # 释放视频源和相关资源
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,213 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
USB camera format and FPS tester for Jetson.
|
||||
|
||||
- Lists available formats/resolutions via v4l2-ctl.
|
||||
- For selected modes, opens the camera with OpenCV and measures per-frame
|
||||
capture time (ms) to show real delivered FPS/latency.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import statistics
|
||||
from typing import List, Tuple
|
||||
|
||||
import cv2
|
||||
|
||||
|
||||
def run_v4l2_list_formats(device: str) -> str:
|
||||
"""Return the raw output of v4l2-ctl --list-formats-ext."""
|
||||
try:
|
||||
out = subprocess.check_output(
|
||||
["v4l2-ctl", f"--device={device}", "--list-formats-ext"],
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
)
|
||||
return out
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print(f"[WARN] Failed to run v4l2-ctl: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def parse_formats(output: str) -> List[Tuple[str, int, int, List[float]]]:
|
||||
"""
|
||||
Parse v4l2-ctl --list-formats-ext output.
|
||||
|
||||
Returns list of (fourcc, width, height, fps_list).
|
||||
"""
|
||||
lines = output.splitlines()
|
||||
formats: List[Tuple[str, int, int, List[float]]] = []
|
||||
|
||||
current_fourcc = None
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith("[") and "):" in line and "'" in line:
|
||||
# Example: [0]: 'MJPG' (Motion-JPEG, compressed)
|
||||
try:
|
||||
fourcc = line.split("'")[1]
|
||||
current_fourcc = fourcc
|
||||
except Exception:
|
||||
current_fourcc = None
|
||||
elif line.startswith("Size: Discrete") and current_fourcc:
|
||||
# Example: Size: Discrete 1280x720
|
||||
try:
|
||||
parts = line.split()
|
||||
wh = parts[-1]
|
||||
w_str, h_str = wh.split("x")
|
||||
width = int(w_str)
|
||||
height = int(h_str)
|
||||
formats.append((current_fourcc, width, height, []))
|
||||
except Exception:
|
||||
pass
|
||||
elif "Interval:" in line and "fps" in line and formats:
|
||||
# Example: Interval: Discrete 0.033s (30.000 fps)
|
||||
try:
|
||||
fps_part = line.split("(")[-1].split("fps")[0]
|
||||
fps = float(fps_part)
|
||||
fourcc, w, h, fps_list = formats[-1]
|
||||
fps_list.append(fps)
|
||||
formats[-1] = (fourcc, w, h, fps_list)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return formats
|
||||
|
||||
|
||||
def fourcc_to_int(code: str) -> int:
|
||||
return cv2.VideoWriter_fourcc(*code)
|
||||
|
||||
|
||||
def benchmark_mode(
|
||||
device_index: int,
|
||||
fourcc: str,
|
||||
width: int,
|
||||
height: int,
|
||||
target_fps: float,
|
||||
warmup: int = 10,
|
||||
measure_frames: int = 60,
|
||||
) -> None:
|
||||
"""Open a specific mode and measure per-frame capture time."""
|
||||
print(f"\n=== Testing {fourcc} {width}x{height} @ {target_fps:.1f} fps ===")
|
||||
|
||||
cap = cv2.VideoCapture(device_index, cv2.CAP_V4L2)
|
||||
if not cap.isOpened():
|
||||
print(" [ERROR] Failed to open device with V4L2 backend.")
|
||||
return
|
||||
|
||||
cap.set(cv2.CAP_PROP_FOURCC, fourcc_to_int(fourcc))
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
|
||||
cap.set(cv2.CAP_PROP_FPS, target_fps)
|
||||
|
||||
# Query negotiated parameters
|
||||
negotiated_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
negotiated_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
negotiated_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
negotiated_fourcc_val = int(cap.get(cv2.CAP_PROP_FOURCC))
|
||||
negotiated_fourcc = "".join(
|
||||
chr((negotiated_fourcc_val >> 8 * i) & 0xFF) for i in range(4)
|
||||
)
|
||||
print(
|
||||
f" Negotiated: {negotiated_w}x{negotiated_h} {negotiated_fourcc} "
|
||||
f"@ {negotiated_fps:.1f} fps (backend={cap.getBackendName()})"
|
||||
)
|
||||
|
||||
# Warm-up frames (to let auto-exposure, buffers, etc. settle)
|
||||
for i in range(warmup):
|
||||
ret, _ = cap.read()
|
||||
if not ret:
|
||||
print(f" [WARN] Warmup frame {i} failed.")
|
||||
time.sleep(0.05)
|
||||
|
||||
times_ms: List[float] = []
|
||||
for i in range(measure_frames):
|
||||
t0 = time.perf_counter()
|
||||
ret, _ = cap.read()
|
||||
t1 = time.perf_counter()
|
||||
if not ret:
|
||||
print(f" [WARN] Capture failed at frame {i}.")
|
||||
break
|
||||
times_ms.append((t1 - t0) * 1000.0)
|
||||
|
||||
cap.release()
|
||||
|
||||
if not times_ms:
|
||||
print(" [ERROR] No successful frames captured.")
|
||||
return
|
||||
|
||||
mean_ms = statistics.mean(times_ms)
|
||||
med_ms = statistics.median(times_ms)
|
||||
min_ms = min(times_ms)
|
||||
max_ms = max(times_ms)
|
||||
eff_fps = 1000.0 / mean_ms
|
||||
|
||||
print(
|
||||
f" Frames measured: {len(times_ms)}\n"
|
||||
f" Capture time (ms): mean={mean_ms:.2f}, median={med_ms:.2f}, "
|
||||
f"min={min_ms:.2f}, max={max_ms:.2f}\n"
|
||||
f" Effective FPS from capture timing: {eff_fps:.2f}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="List USB camera formats and benchmark capture times."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device",
|
||||
default="/dev/video4",
|
||||
help="V4L2 device path (default: /dev/video0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--index",
|
||||
type=int,
|
||||
default=0,
|
||||
help="OpenCV device index (default: 0)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--test-modes",
|
||||
nargs="*",
|
||||
default=["MJPG:640x480:30", "MJPG:1280x720:30"],
|
||||
help=(
|
||||
"Modes to benchmark as FOURCC:WxH:FPS. "
|
||||
"Example: MJPG:640x480:30 YUYV:640x480:30"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print(f"== v4l2-ctl formats for {args.device} ==")
|
||||
raw = run_v4l2_list_formats(args.device)
|
||||
if not raw:
|
||||
print("No v4l2-ctl output; skipping format list.")
|
||||
else:
|
||||
print(raw)
|
||||
parsed = parse_formats(raw)
|
||||
if parsed:
|
||||
print("\n== Parsed formats (FOURCC WxH [fps...]) ==")
|
||||
for fourcc, w, h, fps_list in parsed:
|
||||
fps_str = ", ".join(f"{f:.1f}" for f in fps_list) if fps_list else "?"
|
||||
print(f" {fourcc} {w}x{h} [{fps_str}]")
|
||||
|
||||
print("\n== Benchmarking capture time with OpenCV ==")
|
||||
for mode_str in args.test_modes:
|
||||
try:
|
||||
fourcc_s, wh_s, fps_s = mode_str.split(":")
|
||||
w_s, h_s = wh_s.lower().split("x")
|
||||
w = int(w_s)
|
||||
h = int(h_s)
|
||||
fps = float(fps_s)
|
||||
except ValueError:
|
||||
print(f"[WARN] Cannot parse test mode '{mode_str}', expected FOURCC:WxH:FPS")
|
||||
continue
|
||||
|
||||
benchmark_mode(args.index, fourcc_s, w, h, fps)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user.", file=sys.stderr)
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 929 KiB After Width: | Height: | Size: 929 KiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
@@ -0,0 +1,44 @@
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
# Parameters
|
||||
R = 9.0 # mm
|
||||
H = 20.0 # mm
|
||||
fov_half = np.radians(75) # 75 degrees half-angle
|
||||
pixels_per_mm = 100
|
||||
|
||||
# Texture dimensions
|
||||
width = int(np.pi * 2 * R * pixels_per_mm) # circumference
|
||||
height = int(H * pixels_per_mm)
|
||||
|
||||
# Desired dot pattern in image space (normalized)
|
||||
delta_theta = np.radians(10) # 10° azimuth spacing
|
||||
delta_r = 0.1 # radial spacing in normalized units
|
||||
rho = 0.08 # normalized dot radius (from reference)
|
||||
|
||||
# Generate dot centers in image space
|
||||
r_vals = np.arange(R/H, np.tan(fov_half), delta_r)
|
||||
theta_vals = np.arange(0, 2*np.pi, delta_theta)
|
||||
|
||||
# Create empty texture (white background)
|
||||
img = Image.new('L', (width, height), color=255)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
for r_img in r_vals:
|
||||
z = R / r_img
|
||||
v = z / H
|
||||
# Ellipse axes in texture coordinates
|
||||
a_u = (rho * z) / (2 * np.pi * R)
|
||||
a_v = (rho * z**2) / (R * H)
|
||||
# Convert to pixels
|
||||
a_u_px = a_u * width
|
||||
a_v_px = a_v * height
|
||||
for theta in theta_vals:
|
||||
u = theta / (2 * np.pi)
|
||||
cx = u * width
|
||||
cy = v * height
|
||||
# Draw ellipse (bounding box)
|
||||
bbox = [cx - a_u_px, cy - a_v_px, cx + a_u_px, cy + a_v_px]
|
||||
draw.ellipse(bbox, fill=0) # black dot
|
||||
|
||||
img.save('cylinder_marker_texture.png')
|
||||
@@ -1,102 +0,0 @@
|
||||
"""
|
||||
Verify that the OpenCL FFT convolution in solve_opencl / solve_opencl_batch2
|
||||
produces the same result as scipy.signal.fftconvolve(f, G, mode='same').
|
||||
|
||||
This script simulates the OpenCL logic with numpy FFTs so we can compare
|
||||
directly without needing a GPU.
|
||||
"""
|
||||
import numpy as np
|
||||
from scipy.signal import fftconvolve
|
||||
|
||||
def make_green_2d(H, W, dx=1.0):
|
||||
"""Reproduce PoissonSolver._create_radialGrid for 2D."""
|
||||
rdims = (2*H - 1, 2*W - 1)
|
||||
R = np.indices(rdims, dtype=np.float64)
|
||||
R[0] += 1 - H
|
||||
R[1] += 1 - W
|
||||
R[0] *= dx
|
||||
R[1] *= dx
|
||||
np.hypot(R[1], R[0], R[0])
|
||||
R = R[0]
|
||||
zval = 0.5 * (dx + dx) / 2.0
|
||||
R[H-1, W-1] = zval
|
||||
# Apply 2D Green's function: log(r)/(2*pi)
|
||||
G = np.log(R) * (0.5 / np.pi)
|
||||
return G
|
||||
|
||||
def test_equivalence(H, W, dx=1.0, seed=42):
|
||||
np.random.seed(seed)
|
||||
f = np.random.randn(H, W).astype(np.float64)
|
||||
G = make_green_2d(H, W, dx)
|
||||
gdx_prod = dx ** 2
|
||||
|
||||
# --- CPU reference: scipy fftconvolve ---
|
||||
p_cpu = fftconvolve(f, G, mode='same') * gdx_prod
|
||||
|
||||
# --- Simulate what OpenCL solve_opencl does ---
|
||||
f32 = f.astype(np.float32)
|
||||
G32 = G.astype(np.float32)
|
||||
full_shape = (2*H - 1, 2*W - 1)
|
||||
|
||||
# G already has shape full_shape, no padding needed
|
||||
# f is zero-padded to full_shape at [0:H, 0:W]
|
||||
f_pad = np.zeros(full_shape, dtype=np.float32)
|
||||
f_pad[:H, :W] = f32
|
||||
|
||||
# FFT at full_shape, multiply, iFFT
|
||||
F_f = np.fft.rfftn(f_pad)
|
||||
F_g = np.fft.rfftn(G32)
|
||||
conv_circ = np.fft.irfftn(F_f * F_g, s=full_shape)
|
||||
|
||||
# OLD OpenCL extraction (BUGGY): same_starts = [(full_shape[i] - fshape[i]) // 2]
|
||||
same_starts_old = [(full_shape[0] - H) // 2, (full_shape[1] - W) // 2]
|
||||
p_old = conv_circ[
|
||||
same_starts_old[0]:same_starts_old[0]+H,
|
||||
same_starts_old[1]:same_starts_old[1]+W
|
||||
].astype(np.float32) * np.float32(gdx_prod)
|
||||
|
||||
# FIXED OpenCL extraction: same_starts = [s - 1 for s in fshape]
|
||||
same_starts_fixed = [H - 1, W - 1]
|
||||
p_fixed = conv_circ[
|
||||
same_starts_fixed[0]:same_starts_fixed[0]+H,
|
||||
same_starts_fixed[1]:same_starts_fixed[1]+W
|
||||
].astype(np.float32) * np.float32(gdx_prod)
|
||||
|
||||
# --- Also test with correct FFT size (3H-2) ---
|
||||
correct_fft_shape = (H + (2*H-1) - 1, W + (2*W-1) - 1) # (3H-2, 3W-2)
|
||||
F_f_full = np.fft.rfftn(f32, s=correct_fft_shape)
|
||||
F_g_full = np.fft.rfftn(G32, s=correct_fft_shape)
|
||||
conv_full = np.fft.irfftn(F_f_full * F_g_full, s=correct_fft_shape)
|
||||
# scipy-style 'same' extraction from full linear conv
|
||||
start_full = [(correct_fft_shape[0] - H) // 2, (correct_fft_shape[1] - W) // 2]
|
||||
p_full_linear = conv_full[
|
||||
start_full[0]:start_full[0]+H,
|
||||
start_full[1]:start_full[1]+W
|
||||
].astype(np.float32) * np.float32(gdx_prod)
|
||||
|
||||
# --- Compare ---
|
||||
p_cpu32 = p_cpu.astype(np.float32)
|
||||
scale = max(np.abs(p_cpu32).max(), 1e-10)
|
||||
|
||||
diff_old = np.abs(p_cpu32 - p_old)
|
||||
diff_fixed = np.abs(p_cpu32 - p_fixed)
|
||||
diff_full = np.abs(p_cpu32 - p_full_linear)
|
||||
|
||||
print(f"=== Test: f({H},{W}), G({2*H-1},{2*W-1}), full_shape={full_shape} ===")
|
||||
print(f" scipy fftconvolve full linear conv shape: {correct_fft_shape}")
|
||||
print(f" p_cpu range: [{p_cpu32.min():.4f}, {p_cpu32.max():.4f}]")
|
||||
print()
|
||||
print(f" [OLD BUG] same_starts={same_starts_old}:")
|
||||
print(f" max_abs_diff = {diff_old.max():.6e} rel_max = {diff_old.max()/scale:.6e}")
|
||||
print(f" [FIXED] same_starts={same_starts_fixed}:")
|
||||
print(f" max_abs_diff = {diff_fixed.max():.6e} rel_max = {diff_fixed.max()/scale:.6e}")
|
||||
print(f" [REF] Full linear conv (FFT size {correct_fft_shape}):")
|
||||
print(f" max_abs_diff = {diff_full.max():.6e} rel_max = {diff_full.max()/scale:.6e}")
|
||||
print()
|
||||
|
||||
return diff_old.max(), diff_fixed.max(), diff_full.max()
|
||||
|
||||
if __name__ == '__main__':
|
||||
for size in [16, 32, 64, 128, 256, 384]:
|
||||
d_ocl, d_cor, d_full = test_equivalence(size, size)
|
||||
print()
|
||||