update blender marker design

This commit is contained in:
ykcheungab
2026-06-11 10:10:52 +08:00
parent 9fe93b23e4
commit 4cff6a1625
13 changed files with 1293 additions and 47 deletions
+14 -26
View File
@@ -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. # Minimum y-axis span so autoscale does not over-zoom on small fluctuations.
Y_AXIS_MIN_SPAN = 20000 Y_AXIS_MIN_SPAN = 20000
CURVE_THEME_COLOR = '#00E5FF' CURVE_THEME_COLOR = '#00E5FF'
NORMAL_FORCE_CAL_SLOPE = 3.92685554668795e-05
NORMAL_FORCE_CAL_OFFSET = 0.0217596967278535
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, 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) 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: class TeeStream:
"""同时写入多个流""" """同时写入多个流"""
def __init__(self, *streams): def __init__(self, *streams):
@@ -167,7 +174,7 @@ QMainWindow {
} }
/* ---- 图像面板 ---- */ /* ---- 图像面板 ---- */
QLabel#arrowsLabel, QLabel#magLabel { QLabel#arrowsLabel {
border: 1px solid #262626; border: 1px solid #262626;
border-radius: 4px; border-radius: 4px;
background-color: #0b0b0b; background-color: #0b0b0b;
@@ -401,13 +408,6 @@ class MainWindow(QMainWindow):
self.arrows_label.setAlignment(Qt.AlignCenter) self.arrows_label.setAlignment(Qt.AlignCenter)
left_layout.addWidget(self.arrows_label) 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) h_splitter.addWidget(left)
# 右侧力曲线 # 右侧力曲线
@@ -437,7 +437,7 @@ class MainWindow(QMainWindow):
root_layout.addWidget(v_splitter) root_layout.addWidget(v_splitter)
def _create_plots(self): def _create_plots(self):
titles = ["法向力", "切向力 X", "切向力 Y"] titles = ["法向力 (N)", "切向力 X", "切向力 Y"]
self.plots = [] self.plots = []
self.curves = [] self.curves = []
self.dot_glows = [] self.dot_glows = []
@@ -496,13 +496,13 @@ class MainWindow(QMainWindow):
self.graph_widget.nextRow() self.graph_widget.nextRow()
def adjust_square_images(self): def adjust_square_images(self):
"""根据窗口高度调整左侧面板宽度,使两张图保持正方形""" """根据窗口高度调整左侧面板宽度,使左侧图像保持正方形"""
top_bar_h = 48 top_bar_h = 48
log_h = self.log_widget.height() log_h = self.log_widget.height()
spacing = 20 spacing = 20
available = self.height() - top_bar_h - log_h - spacing available = self.height() - top_bar_h - log_h - spacing
each_h = max(available // 2, 160) image_w = max(available, 160)
self.h_splitter.setSizes([each_h, self.width() - each_h]) 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) is_touched = sensor.compute_deformation(check_motion=True, threshold=1)
sensor.compute_contact() 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.FPS, sensor.info.FNORMAL,
sensor.info.FSHEARX, sensor.info.FSHEARY, sensor.info.FSHEARX, sensor.info.FSHEARY,
sensor.info.VRAW, sensor.info.VNORMAL, sensor.info.VRAW, sensor.info.VNORMAL,
sensor.info.VSHEAR, sensor.info.VSHEAR,
sensor.info.DEPTH sensor.info.DEPTH
) )
fn = calibrate_normal_force(fn_raw)
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}") 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) arrows = cv2.resize(arrows, (400, 400), interpolation=cv2.INTER_CUBIC)
self.arrows_label.setPixmap(cv2_to_pixmap(arrows)) 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 个真实样本,满后再左移滚动 # 曲线:先顺序写入 npoints 个真实样本,满后再左移滚动
current_time = time.time() - self.start_time current_time = time.time() - self.start_time
if self._plot_sample_count < self.npoints: if self._plot_sample_count < self.npoints:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -122,6 +122,66 @@ def fill_polygon_value(grid, width, height, polygon, value):
x_end = max(0, min(width - 1, x_end)) x_end = max(0, min(width - 1, x_end))
grid[y, x_start:x_end + 1] = value 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): def draw_ellipse(pixels, width, height, cx, cy, a, b, angle, color):
"""Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad).""" """Filled ellipse with semi‑axes a,b (pixels), rotation angle (rad)."""
cos_a = np.cos(angle) 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): def convex_hull_uv(points):
"""Return UV points ordered around their 2D convex hull.""" """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: if len(unique) < 3:
return None return None
@@ -282,7 +343,7 @@ def rasterize_mesh_uvs(obj, pixels, tex_width, tex_height, color=(1,1,1,1)):
uv = uv_data[loop_idx].uv uv = uv_data[loop_idx].uv
uv_poly.append((uv.x, uv.y)) uv_poly.append((uv.x, uv.y))
# Convert UV (0‑1) to pixel coordinates # Convert UV (0‑1) to pixel coordinates
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly] 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) fill_polygon(None, pixels, tex_width, tex_height, pixel_poly, color)
def rasterize_mesh_mask(obj, tex_width, tex_height): def rasterize_mesh_mask(obj, tex_width, tex_height):
@@ -298,7 +359,7 @@ def rasterize_mesh_mask(obj, tex_width, tex_height):
for loop_idx in poly.loop_indices: for loop_idx in poly.loop_indices:
uv = uv_data[loop_idx].uv uv = uv_data[loop_idx].uv
uv_poly.append((uv.x, uv.y)) uv_poly.append((uv.x, uv.y))
pixel_poly = [(u * (tex_width - 1), v * (tex_height - 1)) for u, v in uv_poly] 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) fill_polygon_value(mask, tex_width, tex_height, pixel_poly, True)
return mask return mask
@@ -495,9 +556,9 @@ def generate_patch_texture(
for i in range(nx): for i in range(nx):
uv_poly = patch_uv_polygons[idx]; idx += 1 uv_poly = patch_uv_polygons[idx]; idx += 1
if uv_poly is None: continue 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] color = patch_colors[j,i]
rgba = (color[0], color[1], color[2], 1.0) rgba = (color[0], color[1], color[2], 1.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(image, pixels, texture_width, texture_height, pixel_poly, rgba)
# (after drawing all patches) # (after drawing all patches)
@@ -583,16 +644,17 @@ def generate_checker_texture(
uv_poly = patch_uv_polygons[idx]; idx += 1 uv_poly = patch_uv_polygons[idx]; idx += 1
if uv_poly is None: if uv_poly is None:
continue continue
pixel_poly = [(u*(texture_width-1), v*(texture_height-1)) for u,v in uv_poly]
color = patch_colors[j,i] color = patch_colors[j,i]
rgba = (color[0], color[1], color[2], 1.0) rgba = (color[0], color[1], color[2], 1.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(image, pixels, texture_width, texture_height, pixel_poly, rgba)
fill_polygon_value( fill_polygon_value(
patch_state, patch_state,
texture_width, texture_width,
texture_height, texture_height,
pixel_poly, pixel_poly,
1 if np.mean(color) >= 0.5 else 0, patch_value,
) )
complete_checker_gaps( complete_checker_gaps(
@@ -748,10 +810,10 @@ def generate_dot_texture(
def main(): def main():
# ==================== CONFIGURATION ==================== # ==================== CONFIGURATION ====================
MODE = "patches" # "patches", "checker", or "dots" MODE = "patches" # "patches", "checker", or "dots"
OBJ_NAME = "FingerMesh" OBJ_NAME = "FingerMesh0525.001"
CAM_NAME = "Camera" CAM_NAME = "Camera0607"
TEXTURE_WIDTH = 4096 TEXTURE_WIDTH = 1640*2
TEXTURE_HEIGHT = 4096 TEXTURE_HEIGHT = 1600*2
OUTPUT_NAMES = { OUTPUT_NAMES = {
"patches": "patch_texture.png", "patches": "patch_texture.png",
"checker": "checker_texture.png", "checker": "checker_texture.png",
@@ -760,7 +822,7 @@ def main():
OUTPUT_PATH = "//" + OUTPUT_NAMES.get(MODE, "texture.png") OUTPUT_PATH = "//" + OUTPUT_NAMES.get(MODE, "texture.png")
# Patch mode # 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 EDGE_SAMPLE_GRID = 5
# Checker mode # Checker mode
CHECKER_NX = 40; CHECKER_NY = 40; CHECKER_GAP_FILL_PX = 6 CHECKER_NX = 40; CHECKER_NY = 40; CHECKER_GAP_FILL_PX = 6
File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 898 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1002 KiB