dsh ui improve: add brand bar

This commit is contained in:
ykcheungab
2026-09-12 13:45:54 +08:00
parent 803a08fc78
commit a2db648560
48 changed files with 4700 additions and 109 deletions
@@ -13,6 +13,7 @@ __all__ = [
"SdkPacket",
"depth_map_to_bgr",
"draw_arrow_display",
"flow_colormap",
"hex_to_bgr",
"load_fixed_mode",
"orisys_importable",
@@ -28,6 +29,7 @@ _LAZY_ATTRS = {
"orisys_importable": ("orisys_app_shared.acquisition.sdk_worker", "orisys_importable"),
"depth_map_to_bgr": ("orisys_app_shared.acquisition.arrows", "depth_map_to_bgr"),
"draw_arrow_display": ("orisys_app_shared.acquisition.arrows", "draw_arrow_display"),
"flow_colormap": ("orisys_app_shared.acquisition.arrows", "flow_colormap"),
"hex_to_bgr": ("orisys_app_shared.acquisition.arrows", "hex_to_bgr"),
}
@@ -2,24 +2,26 @@
from __future__ import annotations
from functools import lru_cache
from typing import Optional, Sequence
import cv2
import numpy as np
from orisys_app_shared.theme import color
ARROW_BG_BGR = (0x16, 0x13, 0x12)
_ARROW_DRAW_KW = dict(
threshold=2,
grid_spacing=20,
arrow_scale=0.5,
below_threshold_color=(160, 160, 160),
)
_FINGER_ARROW_DRAW_KW = dict(
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
below_threshold_color=(160, 160, 160),
)
"""Fallback canvas colour (dark theme ``arrow_canvas``)."""
FLOW_RAMP_FALLBACK = "#12315D,#6E4AD0,#FA02F3,#FF8A6B,#D8FF00"
"""Design-spec flow ramp: brand navy, violet, magenta, coral, lime."""
FLOW_IDLE_HEX = "#8A93A6"
"""Brand-neutral tint for the grid dots below the flow threshold."""
_LUT_STEPS = 256
_RAMP_MIN_STOPS = 2
_RAMP_MAX_STOPS = 8
def hex_to_bgr(hex_color: str, fallback: tuple[int, int, int] = ARROW_BG_BGR) -> tuple[int, int, int]:
@@ -35,6 +37,62 @@ def hex_to_bgr(hex_color: str, fallback: tuple[int, int, int] = ARROW_BG_BGR) ->
return (b, g, r)
_ARROW_DRAW_KW = dict(
threshold=2,
grid_spacing=20,
arrow_scale=0.5,
below_threshold_color=hex_to_bgr(FLOW_IDLE_HEX),
)
_FINGER_ARROW_DRAW_KW = dict(
threshold=0.4,
grid_spacing=10,
arrow_scale=1.0,
below_threshold_color=hex_to_bgr(FLOW_IDLE_HEX),
)
def _parse_ramp(ramp: str) -> list[tuple[int, int, int]]:
stops: list[tuple[int, int, int]] = []
for token in str(ramp or "").split(","):
text = token.strip().lstrip("#")
if len(text) != 6:
continue
try:
stops.append((int(text[0:2], 16), int(text[2:4], 16), int(text[4:6], 16)))
except ValueError:
continue
return stops
@lru_cache(maxsize=8)
def _lut_from_ramp(ramp: str) -> np.ndarray:
"""Build a 256-entry user colour table from ``#RRGGBB`` ramp stops.
The table is BGR, matching how :func:`cv2.applyColorMap` consumes user
colormaps. Invalid input never raises: it falls back to
:data:`FLOW_RAMP_FALLBACK`.
"""
stops = _parse_ramp(ramp)
if not _RAMP_MIN_STOPS <= len(stops) <= _RAMP_MAX_STOPS:
stops = _parse_ramp(FLOW_RAMP_FALLBACK)
positions = np.linspace(0.0, 1.0, _LUT_STEPS)
span = np.linspace(0.0, 1.0, len(stops))
lut = np.empty((_LUT_STEPS, 1, 3), dtype=np.uint8)
for channel in range(3): # source order: R, G, B
values = np.interp(positions, span, [stop[channel] for stop in stops])
lut[:, 0, channel] = np.clip(np.round(values), 0, 255).astype(np.uint8)
return lut[:, :, ::-1].copy()
def flow_colormap(theme: str = "dark") -> np.ndarray:
"""Return the branded flow-magnitude colour table for ``theme``.
Cached on the resolved ramp string rather than the theme name, so a theme
switch is picked up as soon as :func:`build_qss` reloads the tokens.
"""
return _lut_from_ramp(color(theme, "flow_ramp", FLOW_RAMP_FALLBACK))
def _ensure_bgr(img: np.ndarray) -> np.ndarray:
if img.ndim == 2:
return cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
@@ -53,19 +111,22 @@ def draw_arrow_display(
canvas_bgr: Sequence[int] | None = None,
draw_style: str = "box",
valid_mask: Optional[np.ndarray] = None,
colormap: Optional[np.ndarray] = None,
) -> np.ndarray:
"""Draw flow arrows on a solid canvas or over the processed frame."""
base = _ensure_bgr(processed_frame)
if flow is None:
return base
draw_kw = _FINGER_ARROW_DRAW_KW if str(draw_style).lower().startswith("f") else _ARROW_DRAW_KW
draw_kw = dict(_FINGER_ARROW_DRAW_KW if str(draw_style).lower().startswith("f") else _ARROW_DRAW_KW)
if colormap is not None:
draw_kw["colormap"] = colormap
try:
import orisys
if background == "pure_color":
canvas = np.empty_like(base)
color = ARROW_BG_BGR if canvas_bgr is None else tuple(int(v) for v in canvas_bgr)
canvas[:, :] = color
color_bgr = ARROW_BG_BGR if canvas_bgr is None else tuple(int(v) for v in canvas_bgr)
canvas[:, :] = color_bgr
return orisys.util.draw_arrows(
canvas, flow, valid_mask=valid_mask, **draw_kw
)
@@ -74,8 +135,11 @@ def draw_arrow_display(
return base
def depth_map_to_bgr(depth: Optional[np.ndarray]) -> Optional[np.ndarray]:
"""Convert a float depth/divergence map to a JET colormap BGR image."""
def depth_map_to_bgr(
depth: Optional[np.ndarray],
colormap: Optional[np.ndarray] = None,
) -> Optional[np.ndarray]:
"""Convert a float depth/divergence map to a colormap BGR image."""
if depth is None:
return None
arr = np.asarray(depth, dtype=np.float64)
@@ -90,4 +154,6 @@ def depth_map_to_bgr(depth: Optional[np.ndarray]) -> Optional[np.ndarray]:
normalized = ((arr - lo) / (hi - lo) * 255.0).astype(np.uint8)
else:
normalized = np.zeros(arr.shape, dtype=np.uint8)
return cv2.applyColorMap(normalized, cv2.COLORMAP_JET)
return cv2.applyColorMap(
normalized, cv2.COLORMAP_JET if colormap is None else colormap
)
@@ -1,45 +1,51 @@
{
"window_bg": "#121316",
"panel_bg": "#1a1c20",
"panel_border": "#2a2d33",
"text_primary": "#e8eaed",
"text_secondary": "#9aa0a6",
"text_muted": "#6f757c",
"accent": "#4a62c0",
"accent_hover": "#5c74d0",
"accent_text": "#ffffff",
"toolbar_bg": "#1a1c20",
"button_bg": "#2a2d33",
"button_hover": "#3a3f47",
"input_bg": "#0f1012",
"input_border": "#3a3f47",
"menu_bg": "#1a1c20",
"menu_hover": "#2a2d33",
"plot_fn": "#8ab4f8",
"plot_fx": "#34a853",
"plot_fy": "#fbbc04",
"plot_bg": "#15171a",
"plot_grid": "#2a2d33",
"arrow_canvas": "#121316",
"danger": "#ea4335",
"danger_hover": "#f2554a",
"danger_text": "#ffffff",
"warning": "#fbbc04",
"info": "#8ab4f8",
"success": "#34a853",
"status_idle": "#9aa0a6",
"status_connecting": "#fbbc04",
"status_running": "#34a853",
"status_error": "#ea4335",
"table_header_bg": "#23262b",
"table_row_alt": "#1e2126",
"table_selected": "#33406b",
"scrollbar_bg": "#1a1c20",
"scrollbar_handle": "#3a3f47",
"scrollbar_handle_hover": "#4a5058",
"card_bg": "#1e2126",
"card_border": "#32363d",
"separator": "#2a2d33",
"focus_ring": "#6b82d9",
"hint_text": "#7d838a"
"window_bg": "#0E1320",
"panel_bg": "#161C2A",
"panel_border": "#28303F",
"toolbar_bg": "#161C2A",
"card_bg": "#1B2231",
"card_border": "#2E3748",
"separator": "#28303F",
"input_bg": "#0B0F19",
"input_border": "#66739A",
"button_bg": "#28303F",
"button_hover": "#333D50",
"menu_bg": "#161C2A",
"menu_hover": "#28303F",
"table_header_bg": "#1F2635",
"table_row_alt": "#1A2130",
"table_selected": "#2C3A63",
"scrollbar_bg": "#161C2A",
"scrollbar_handle": "#627092",
"scrollbar_handle_hover": "#6E7C9E",
"plot_bg": "#11161F",
"plot_grid": "#28303F",
"plot_fn": "#8AA6F2",
"plot_fx": "#F45CC0",
"plot_fy": "#D8FF00",
"text_primary": "#E6EAF2",
"text_secondary": "#9AA4B8",
"text_muted": "#838FA6",
"hint_text": "#838FA6",
"accent": "#4D62BE",
"accent_hover": "#5770CE",
"accent_text": "#FFFFFF",
"focus_ring": "#7C92E8",
"danger": "#EE6B60",
"danger_hover": "#F0796E",
"danger_fill": "#CC4438",
"danger_fill_hover": "#BE3A31",
"danger_text": "#FFFFFF",
"warning": "#F0B429",
"info": "#8AA6F2",
"success": "#4FC08D",
"status_idle": "#9AA4B8",
"status_connecting": "#F0B429",
"status_running": "#4FC08D",
"status_error": "#EE6B60",
"brand_bar_bg": "#12315D",
"brand_bar_fg": "#FFFFFF",
"brand_bar_fg_muted": "#B9C8E4",
"arrow_canvas": "#0B1830",
"flow_ramp": "#0B1830,#6E4AD0,#FA02F3,#FF8A6B,#D8FF00"
}
@@ -1,45 +1,51 @@
{
"window_bg": "#ECEFF4",
"window_bg": "#E1E6EF",
"panel_bg": "#FFFFFF",
"panel_border": "#D0D5DE",
"text_primary": "#1A1C20",
"text_secondary": "#5F6368",
"text_muted": "#7C838C",
"accent": "#003893",
"accent_hover": "#2A4FA8",
"accent_text": "#ffffff",
"toolbar_bg": "#E8EBF0",
"button_bg": "#F4F6FA",
"button_hover": "#E8ECF5",
"panel_border": "#CFD7E4",
"toolbar_bg": "#EDF1F8",
"card_bg": "#F4F7FC",
"card_border": "#D5DCE8",
"separator": "#D8DFEA",
"input_bg": "#FFFFFF",
"input_border": "#C5CAD3",
"input_border": "#7C8AA4",
"button_bg": "#F6F8FD",
"button_hover": "#E7EDF9",
"menu_bg": "#FFFFFF",
"menu_hover": "#EEF1F7",
"menu_hover": "#EDF1F8",
"table_header_bg": "#EEF2F9",
"table_row_alt": "#F7F9FD",
"table_selected": "#D7E2FA",
"scrollbar_bg": "#E6EBF3",
"scrollbar_handle": "#78839A",
"scrollbar_handle_hover": "#6A7590",
"plot_bg": "#F8FAFD",
"plot_grid": "#DFE5EE",
"plot_fn": "#003893",
"plot_fx": "#188038",
"plot_fy": "#E8710A",
"plot_bg": "#FAFBFD",
"plot_grid": "#E3E6EC",
"arrow_canvas": "#EEF1F7",
"danger": "#D93025",
"danger_hover": "#B3261C",
"danger_text": "#ffffff",
"warning": "#F9AB00",
"info": "#1A73E8",
"success": "#1B8A5A",
"status_idle": "#5F6368",
"status_connecting": "#F9AB00",
"status_running": "#1B8A5A",
"status_error": "#D93025",
"table_header_bg": "#F1F3F8",
"table_row_alt": "#F8F9FC",
"table_selected": "#DCE6FA",
"scrollbar_bg": "#EDEFF3",
"scrollbar_handle": "#C3C9D4",
"scrollbar_handle_hover": "#A8B0BE",
"card_bg": "#F7F9FC",
"card_border": "#DDE2EA",
"separator": "#D8DDE6",
"focus_ring": "#2A4FA8",
"hint_text": "#7C838C"
"plot_fx": "#B0178C",
"plot_fy": "#4E7A00",
"text_primary": "#141A24",
"text_secondary": "#4A5566",
"text_muted": "#5C6678",
"hint_text": "#5C6678",
"accent": "#003893",
"accent_hover": "#4D62BE",
"accent_text": "#FFFFFF",
"focus_ring": "#4D62BE",
"danger": "#C7362B",
"danger_hover": "#B93227",
"danger_fill": "#C7362B",
"danger_fill_hover": "#B93227",
"danger_text": "#FFFFFF",
"warning": "#8A5A00",
"info": "#3A55B8",
"success": "#1F7A4D",
"status_idle": "#4A5566",
"status_connecting": "#8A5A00",
"status_running": "#1F7A4D",
"status_error": "#C7362B",
"brand_bar_bg": "#12315D",
"brand_bar_fg": "#FFFFFF",
"brand_bar_fg_muted": "#B9C8E4",
"arrow_canvas": "#12315D",
"flow_ramp": "#12315D,#6E4AD0,#FA02F3,#FF8A6B,#D8FF00"
}
+37 -9
View File
@@ -146,6 +146,35 @@ QToolTip {{
border-radius: 6px;
padding: 5px 8px;
}}
/* ---------- Brand bar ---------- */
QFrame#brandBar {{
background-color: {c['brand_bar_bg']};
border: none;
border-radius: 8px;
}}
QLabel#brandName {{
color: {c['brand_bar_fg']};
font-size: 15px;
font-weight: 700;
letter-spacing: 1.5px;
}}
QLabel#brandTagline {{
color: {c['brand_bar_fg_muted']};
font-size: 11px;
letter-spacing: 0.5px;
}}
QLabel#brandProduct {{
color: {c['brand_bar_fg']};
font-size: 13px;
font-weight: 600;
letter-spacing: 0.3px;
}}
QFrame#brandBar QFrame#sectionSeparator {{
background-color: {c['brand_bar_fg_muted']};
border: none;
max-width: 1px;
min-width: 1px;
}}
/* ---------- Buttons ---------- */
QPushButton {{
background-color: {c['button_bg']};
@@ -157,7 +186,6 @@ QPushButton {{
}}
QPushButton:hover {{
background-color: {c['button_hover']};
border-color: {c['accent_hover']};
}}
QPushButton:pressed {{
background-color: {c['menu_hover']};
@@ -192,20 +220,20 @@ QPushButton#primaryButton:disabled {{
border-color: {c['panel_border']};
}}
QPushButton#dangerButton {{
background-color: {c['danger']};
background-color: {c['danger_fill']};
color: {c['danger_text']};
border: 1px solid {c['danger']};
border: 1px solid {c['danger_fill']};
font-weight: 600;
padding: 7px 18px;
}}
QPushButton#dangerButton:hover {{
background-color: {c['danger_hover']};
border-color: {c['danger_hover']};
background-color: {c['danger_fill_hover']};
border-color: {c['danger_fill_hover']};
}}
QPushButton#dangerButton:pressed,
QPushButton#dangerButton:checked {{
background-color: {c['danger_hover']};
border-color: {c['danger']};
background-color: {c['danger_fill_hover']};
border-color: {c['danger_fill']};
}}
QPushButton#dangerButton:disabled {{
background-color: {c['panel_border']};
@@ -249,10 +277,10 @@ QComboBox, QLineEdit, QSpinBox, QDoubleSpinBox, QAbstractSpinBox {{
selection-color: {accent_text};
}}
QComboBox:hover, QLineEdit:hover, QSpinBox:hover, QDoubleSpinBox:hover {{
border-color: {c['accent_hover']};
border-color: {c['focus_ring']};
}}
QComboBox:focus, QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus {{
border: 1px solid {c['accent']};
border: 1px solid {c['focus_ring']};
}}
QComboBox:disabled, QLineEdit:disabled, QSpinBox:disabled, QDoubleSpinBox:disabled {{
color: {c['text_muted']};