update blender marker design
@@ -35,6 +35,8 @@ PLOT_WINDOW_SEC = PLOT_BUFFER_POINTS / FORCE_SAMPLE_RATE_HZ # 4 s window
|
||||
# Minimum y-axis span so autoscale does not over-zoom on small fluctuations.
|
||||
Y_AXIS_MIN_SPAN = 20000
|
||||
CURVE_THEME_COLOR = '#00E5FF'
|
||||
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
|
||||
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout,
|
||||
@@ -125,6 +127,11 @@ def lowpass_filter(data, cutoff_hz, sample_rate_hz=FORCE_SAMPLE_RATE_HZ, order=2
|
||||
return filtfilt(b, a, y)
|
||||
|
||||
|
||||
def calibrate_normal_force(raw_value):
|
||||
"""Convert raw normal-force reading to Newtons using y = mx + c."""
|
||||
return NORMAL_FORCE_CAL_SLOPE * raw_value + NORMAL_FORCE_CAL_OFFSET
|
||||
|
||||
|
||||
class TeeStream:
|
||||
"""同时写入多个流"""
|
||||
def __init__(self, *streams):
|
||||
@@ -167,7 +174,7 @@ QMainWindow {
|
||||
}
|
||||
|
||||
/* ---- 图像面板 ---- */
|
||||
QLabel#arrowsLabel, QLabel#magLabel {
|
||||
QLabel#arrowsLabel {
|
||||
border: 1px solid #262626;
|
||||
border-radius: 4px;
|
||||
background-color: #0b0b0b;
|
||||
@@ -401,13 +408,6 @@ class MainWindow(QMainWindow):
|
||||
self.arrows_label.setAlignment(Qt.AlignCenter)
|
||||
left_layout.addWidget(self.arrows_label)
|
||||
|
||||
self.mag_label = QLabel()
|
||||
self.mag_label.setObjectName("magLabel")
|
||||
self.mag_label.setMinimumSize(160, 160)
|
||||
self.mag_label.setScaledContents(True)
|
||||
self.mag_label.setAlignment(Qt.AlignCenter)
|
||||
left_layout.addWidget(self.mag_label)
|
||||
|
||||
h_splitter.addWidget(left)
|
||||
|
||||
# 右侧力曲线
|
||||
@@ -437,7 +437,7 @@ class MainWindow(QMainWindow):
|
||||
root_layout.addWidget(v_splitter)
|
||||
|
||||
def _create_plots(self):
|
||||
titles = ["法向力", "切向力 X", "切向力 Y"]
|
||||
titles = ["法向力 (N)", "切向力 X", "切向力 Y"]
|
||||
self.plots = []
|
||||
self.curves = []
|
||||
self.dot_glows = []
|
||||
@@ -496,13 +496,13 @@ class MainWindow(QMainWindow):
|
||||
self.graph_widget.nextRow()
|
||||
|
||||
def adjust_square_images(self):
|
||||
"""根据窗口高度调整左侧面板宽度,使两张图保持正方形"""
|
||||
"""根据窗口高度调整左侧面板宽度,使左侧图像保持正方形"""
|
||||
top_bar_h = 48
|
||||
log_h = self.log_widget.height()
|
||||
spacing = 20
|
||||
available = self.height() - top_bar_h - log_h - spacing
|
||||
each_h = max(available // 2, 160)
|
||||
self.h_splitter.setSizes([each_h, self.width() - each_h])
|
||||
image_w = max(available, 160)
|
||||
self.h_splitter.setSizes([image_w, self.width() - image_w])
|
||||
|
||||
# ---- 帧处理 ----
|
||||
|
||||
@@ -534,13 +534,14 @@ class MainWindow(QMainWindow):
|
||||
is_touched = sensor.compute_deformation(check_motion=True, threshold=1)
|
||||
sensor.compute_contact()
|
||||
|
||||
fps, fn, fx, fy, flow, vnormal, vshear, depth_map = sensor.read_info(
|
||||
fps, fn_raw, fx, fy, flow, vnormal, vshear, _ = sensor.read_info(
|
||||
sensor.info.FPS, sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX, sensor.info.FSHEARY,
|
||||
sensor.info.VRAW, sensor.info.VNORMAL,
|
||||
sensor.info.VSHEAR,
|
||||
sensor.info.DEPTH
|
||||
)
|
||||
fn = calibrate_normal_force(fn_raw)
|
||||
|
||||
|
||||
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
|
||||
@@ -564,19 +565,6 @@ class MainWindow(QMainWindow):
|
||||
arrows = cv2.resize(arrows, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
self.arrows_label.setPixmap(cv2_to_pixmap(arrows))
|
||||
|
||||
# 显示深度图
|
||||
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:蓝色=低散度,红色=高散度)
|
||||
depth_colored = cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET)
|
||||
|
||||
# mag = draw_magnitude_map(flow)
|
||||
self.mag_label.setPixmap(cv2_to_pixmap(depth_colored))
|
||||
|
||||
# 曲线:先顺序写入 npoints 个真实样本,满后再左移滚动
|
||||
current_time = time.time() - self.start_time
|
||||
if self._plot_sample_count < self.npoints:
|
||||
|
||||
@@ -122,6 +122,66 @@ def fill_polygon_value(grid, width, height, polygon, value):
|
||||
x_end = max(0, min(width - 1, x_end))
|
||||
grid[y, x_start:x_end + 1] = value
|
||||
|
||||
def unwrap_periodic_values(values):
|
||||
"""
|
||||
Unwrap periodic UV coordinates so points straddling the 0/1 seam stay local.
|
||||
"""
|
||||
arr = np.asarray(values, dtype=np.float64).copy()
|
||||
if arr.size == 0:
|
||||
return arr
|
||||
if arr.max() - arr.min() > 0.5:
|
||||
arr[arr < 0.5] += 1.0
|
||||
return arr
|
||||
|
||||
def wrap_uv_polygon_for_raster(uv_poly):
|
||||
"""
|
||||
Return a seam-aware version of a UV polygon.
|
||||
|
||||
UV islands may cross the 0/1 seam. If we rasterize those raw coordinates as a
|
||||
single polygon, Blender's repeated UVs turn into a long diagonal triangle
|
||||
across the texture. We unwrap seam-crossing polygons first, then draw wrapped
|
||||
copies in the texture tile(s) they overlap.
|
||||
"""
|
||||
if len(uv_poly) < 3:
|
||||
return []
|
||||
|
||||
uv_arr = np.array([(float(u), float(v)) for u, v in uv_poly], dtype=np.float64)
|
||||
uv_arr[:, 0] = unwrap_periodic_values(uv_arr[:, 0])
|
||||
uv_arr[:, 1] = unwrap_periodic_values(uv_arr[:, 1])
|
||||
return [tuple(pt) for pt in uv_arr]
|
||||
|
||||
def uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
"""
|
||||
Convert one UV polygon into one or more pixel polygons, duplicating seam-
|
||||
crossing polygons into adjacent tiles so rasterization stays local.
|
||||
"""
|
||||
unwrapped = wrap_uv_polygon_for_raster(uv_poly)
|
||||
if len(unwrapped) < 3:
|
||||
return []
|
||||
|
||||
u_vals = [u for u, _ in unwrapped]
|
||||
v_vals = [v for _, v in unwrapped]
|
||||
u_shift_start = int(np.floor(min(u_vals)))
|
||||
u_shift_end = int(np.floor(max(u_vals)))
|
||||
v_shift_start = int(np.floor(min(v_vals)))
|
||||
v_shift_end = int(np.floor(max(v_vals)))
|
||||
|
||||
pixel_polys = []
|
||||
for u_shift in range(u_shift_start, u_shift_end + 1):
|
||||
for v_shift in range(v_shift_start, v_shift_end + 1):
|
||||
shifted = [(u - u_shift, v - v_shift) for u, v in unwrapped]
|
||||
shifted_u = [u for u, _ in shifted]
|
||||
shifted_v = [v for _, v in shifted]
|
||||
if max(shifted_u) < 0.0 or min(shifted_u) > 1.0:
|
||||
continue
|
||||
if max(shifted_v) < 0.0 or min(shifted_v) > 1.0:
|
||||
continue
|
||||
pixel_polys.append([
|
||||
(u * (tex_width - 1), v * (tex_height - 1))
|
||||
for u, v in shifted
|
||||
])
|
||||
return pixel_polys
|
||||
|
||||
def draw_ellipse(pixels, width, height, cx, cy, a, b, angle, color):
|
||||
"""Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad)."""
|
||||
cos_a = np.cos(angle)
|
||||
@@ -194,7 +254,8 @@ def raycast_uv_from_camera(scene, depsgraph, cam, obj, u_ndc, v_ndc, frame_width
|
||||
|
||||
def convex_hull_uv(points):
|
||||
"""Return UV points ordered around their 2D convex hull."""
|
||||
unique = sorted({(float(p.x), float(p.y)) for p in points})
|
||||
unwrapped = wrap_uv_polygon_for_raster([(p.x, p.y) for p in points])
|
||||
unique = sorted(set(unwrapped))
|
||||
if len(unique) < 3:
|
||||
return None
|
||||
|
||||
@@ -282,8 +343,8 @@ def rasterize_mesh_uvs(obj, pixels, tex_width, tex_height, color=(1,1,1,1)):
|
||||
uv = uv_data[loop_idx].uv
|
||||
uv_poly.append((uv.x, uv.y))
|
||||
# Convert UV (0‑1) to pixel coordinates
|
||||
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
|
||||
|
||||
def rasterize_mesh_mask(obj, tex_width, tex_height):
|
||||
"""Rasterize the active UV map to a boolean mesh mask."""
|
||||
@@ -298,8 +359,8 @@ def rasterize_mesh_mask(obj, tex_width, tex_height):
|
||||
for loop_idx in poly.loop_indices:
|
||||
uv = uv_data[loop_idx].uv
|
||||
uv_poly.append((uv.x, uv.y))
|
||||
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly]
|
||||
fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, tex_width, tex_height):
|
||||
fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
|
||||
return mask
|
||||
|
||||
def paint_mask_color(pixels, width, height, mask, color):
|
||||
@@ -495,10 +556,10 @@ def generate_patch_texture(
|
||||
for i in range(nx):
|
||||
uv_poly = patch_uv_polygons[idx]; idx += 1
|
||||
if uv_poly is None: continue
|
||||
pixel_poly = [(u*(texture_width-1), v*(texture_height-1)) for u,v in uv_poly]
|
||||
color = patch_colors[j,i]
|
||||
rgba = (color[0], color[1], color[2], 1.0)
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, texture_width, texture_height):
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
|
||||
# (after drawing all patches)
|
||||
if uv_centre is not None:
|
||||
@@ -583,17 +644,18 @@ def generate_checker_texture(
|
||||
uv_poly = patch_uv_polygons[idx]; idx += 1
|
||||
if uv_poly is None:
|
||||
continue
|
||||
pixel_poly = [(u*(texture_width-1), v*(texture_height-1)) for u,v in uv_poly]
|
||||
color = patch_colors[j,i]
|
||||
rgba = (color[0], color[1], color[2], 1.0)
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
fill_polygon_value(
|
||||
patch_state,
|
||||
texture_width,
|
||||
texture_height,
|
||||
pixel_poly,
|
||||
1 if np.mean(color) >= 0.5 else 0,
|
||||
)
|
||||
patch_value = 1 if np.mean(color) >= 0.5 else 0
|
||||
for pixel_poly in uv_polygon_to_pixel_polygons(uv_poly, texture_width, texture_height):
|
||||
fill_polygon(image, pixels, texture_width, texture_height, pixel_poly, rgba)
|
||||
fill_polygon_value(
|
||||
patch_state,
|
||||
texture_width,
|
||||
texture_height,
|
||||
pixel_poly,
|
||||
patch_value,
|
||||
)
|
||||
|
||||
complete_checker_gaps(
|
||||
pixels, texture_width, texture_height, mesh_mask, patch_state, max_gap_px=gap_fill_px
|
||||
@@ -748,10 +810,10 @@ def generate_dot_texture(
|
||||
def main():
|
||||
# ==================== CONFIGURATION ====================
|
||||
MODE = "patches" # "patches", "checker", or "dots"
|
||||
OBJ_NAME = "FingerMesh"
|
||||
CAM_NAME = "Camera"
|
||||
TEXTURE_WIDTH = 4096
|
||||
TEXTURE_HEIGHT = 4096
|
||||
OBJ_NAME = "FingerMesh0525.001"
|
||||
CAM_NAME = "Camera0607"
|
||||
TEXTURE_WIDTH = 1640*2
|
||||
TEXTURE_HEIGHT = 1600*2
|
||||
OUTPUT_NAMES = {
|
||||
"patches": "patch_texture.png",
|
||||
"checker": "checker_texture.png",
|
||||
@@ -760,7 +822,7 @@ def main():
|
||||
OUTPUT_PATH = "//" + OUTPUT_NAMES.get(MODE, "texture.png")
|
||||
|
||||
# Patch mode
|
||||
PATCH_NX = 60; PATCH_NY = 50; COLOR_R = 0.05
|
||||
PATCH_NX = 80; PATCH_NY = 45; COLOR_R = 0.05
|
||||
EDGE_SAMPLE_GRID = 5
|
||||
# Checker mode
|
||||
CHECKER_NX = 40; CHECKER_NY = 40; CHECKER_GAP_FILL_PX = 6
|
||||
|
||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 795 KiB |
|
Before Width: | Height: | Size: 898 KiB |
|
Before Width: | Height: | Size: 1002 KiB |