Compare commits

...

2 Commits

Author SHA1 Message Date
warrenchen
11e779bb40 Merge branch 'main' of https://gitea.innovedus.com/warrenchen/web_academy_prototype 2026-03-03 16:05:47 +09:00
warrenchen
f85613b8a6 Add new images, firmware files, and update versioning for KL520, KL630, KL720, and KL730
- Added new images: bike_cars_street_224x224.bmp and one_bike_many_cars_800x800.bmp.
- Updated TEST_PAIRS.md to reflect new source paths for YOLOv5 and FCOS models.
- Introduced VERSION files for KL520 (2.2.0), KL630 (SDK-v2.5.7), and KL720 (2.2.0).
- Added firmware binaries for KL520, KL630, KL720, and KL730.
- Updated third-party Kneron_DFUT resources, including new binaries and translations.
- Added KneronPLUS-1.2.1.zip to third-party resources.
2026-03-03 15:21:59 +09:00
84 changed files with 2985 additions and 37 deletions

View File

@ -0,0 +1,297 @@
from __future__ import annotations
import ctypes
import hashlib
import json
import os
import sys
import time
from pathlib import Path
from typing import Any, Dict
KDP_MAGIC_CONNECTION_PASS = 536173391
KP_SUCCESS = 0
KP_RESET_REBOOT = 0
USB_WAIT_CONNECT_DELAY_MS = 100
USB_WAIT_AFTER_REBOOT_MS = 2000
USB_WAIT_RETRY_CONNECT_MS = 10
MAX_RETRY_CONNECT_TIMES = 10
def _normalize_code(code: int) -> int:
# Some legacy paths may return int8-like unsigned values (e.g. 253 for -3).
if code > 127:
return code - 256
return code
def _load_libkplus(dist_root: Path) -> ctypes.CDLL:
lib_dir = dist_root / "kp" / "lib"
dll_path = lib_dir / "libkplus.dll"
if not dll_path.is_file():
raise RuntimeError(f"libkplus.dll not found: {dll_path}")
if hasattr(os, "add_dll_directory"):
os.add_dll_directory(str(lib_dir))
lib = ctypes.CDLL(str(dll_path))
lib.kp_connect_devices.argtypes = [ctypes.c_int, ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)]
lib.kp_connect_devices.restype = ctypes.c_void_p
lib.kp_set_timeout.argtypes = [ctypes.c_void_p, ctypes.c_int]
lib.kp_set_timeout.restype = None
lib.kp_reset_device.argtypes = [ctypes.c_void_p, ctypes.c_int]
lib.kp_reset_device.restype = ctypes.c_int
lib.kp_load_firmware_from_file.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
lib.kp_load_firmware_from_file.restype = ctypes.c_int
lib.kp_update_kdp_firmware_from_files.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_bool]
lib.kp_update_kdp_firmware_from_files.restype = ctypes.c_int
lib.kp_disconnect_devices.argtypes = [ctypes.c_void_p]
lib.kp_disconnect_devices.restype = ctypes.c_int
lib.kp_scan_devices.argtypes = []
lib.kp_scan_devices.restype = ctypes.c_void_p
if hasattr(lib, "kp_error_string"):
lib.kp_error_string.argtypes = [ctypes.c_int]
lib.kp_error_string.restype = ctypes.c_char_p
return lib
def _errstr(lib: ctypes.CDLL, code: int) -> str:
signed_code = _normalize_code(code)
if hasattr(lib, "kp_error_string"):
try:
msg = lib.kp_error_string(int(code))
if not msg and signed_code != code:
msg = lib.kp_error_string(int(signed_code))
if msg:
return msg.decode("utf-8", errors="replace")
except Exception:
pass
return str(code)
def _find_port_id_with_kp(dist_root: Path, port_id: int | None, scan_index: int | None) -> int:
if port_id is not None:
return int(port_id)
sys.path.insert(0, str(dist_root))
import kp
device_list = kp.core.scan_devices()
if device_list.device_descriptor_number == 0:
raise RuntimeError("NO_DEVICE: no device found")
idx = 0 if scan_index is None else int(scan_index)
if idx < 0 or idx >= device_list.device_descriptor_number:
raise RuntimeError(f"INVALID_SCAN_INDEX: {idx}")
return int(device_list.device_descriptor_list[idx].usb_port_id)
def _file_diag(path_str: str) -> Dict[str, Any]:
p = Path(path_str)
info: Dict[str, Any] = {
"path": str(p),
"name": p.name,
"exists": p.is_file(),
}
if not p.is_file():
return info
data = p.read_bytes()
info["size_bytes"] = len(data)
info["sha256"] = hashlib.sha256(data).hexdigest()
return info
def _scan_diag_with_kp(dist_root: Path) -> Dict[str, Any]:
sys.path.insert(0, str(dist_root))
import kp
scanned = []
dev_list = kp.core.scan_devices()
num = int(dev_list.device_descriptor_number)
for i in range(num):
d = dev_list.device_descriptor_list[i]
scanned.append(
{
"scan_index": i,
"usb_port_id": int(d.usb_port_id),
"vendor_id": int(d.vendor_id),
"product_id": f"0x{int(d.product_id):04X}",
"link_speed": str(d.link_speed),
"usb_port_path": str(d.usb_port_path),
"is_connectable": bool(d.is_connectable),
"firmware": str(d.firmware),
}
)
return {"count": num, "devices": scanned}
def _firmware_from_scan(scan_diag: Dict[str, Any], port_id: int) -> str:
for d in scan_diag.get("devices", []):
if int(d.get("usb_port_id", -1)) == int(port_id):
return str(d.get("firmware", "")).upper()
return ""
def _product_id_from_scan(scan_diag: Dict[str, Any], port_id: int) -> int | None:
for d in scan_diag.get("devices", []):
if int(d.get("usb_port_id", -1)) != int(port_id):
continue
raw = d.get("product_id")
if raw is None:
return None
text = str(raw).strip()
try:
if text.lower().startswith("0x"):
return int(text, 16)
return int(text)
except Exception:
return None
return None
def _connect_with_magic(lib: ctypes.CDLL, port_id: int) -> ctypes.c_void_p:
port_ids = (ctypes.c_int * 1)(int(port_id))
status = ctypes.c_int(KDP_MAGIC_CONNECTION_PASS)
device_group = lib.kp_connect_devices(1, port_ids, ctypes.byref(status))
if not device_group or status.value != KP_SUCCESS:
signed = _normalize_code(status.value)
raise RuntimeError(
f"CONNECT_FAILED: raw_code={status.value}, signed_code={signed}, msg={_errstr(lib, status.value)}"
)
return device_group
def _reboot_and_reconnect(lib: ctypes.CDLL, device_group: ctypes.c_void_p, port_id: int) -> ctypes.c_void_p:
time.sleep(USB_WAIT_CONNECT_DELAY_MS / 1000.0)
ret = lib.kp_reset_device(device_group, KP_RESET_REBOOT)
if ret != KP_SUCCESS:
raise RuntimeError(
f"RESET_FAILED: raw_code={ret}, signed_code={_normalize_code(ret)}, msg={_errstr(lib, ret)}"
)
time.sleep(USB_WAIT_AFTER_REBOOT_MS / 1000.0)
lib.kp_disconnect_devices(device_group)
retries = 0
while retries <= MAX_RETRY_CONNECT_TIMES:
try:
return _connect_with_magic(lib, port_id)
except RuntimeError:
time.sleep(USB_WAIT_RETRY_CONNECT_MS / 1000.0)
retries += 1
raise RuntimeError("RECONNECT_FAILED: max retry exceeded")
def main() -> None:
stage = "init"
diag: Dict[str, Any] = {}
try:
if len(sys.argv) != 2:
raise RuntimeError("missing json payload argument")
req: Dict[str, Any] = json.loads(sys.argv[1])
dist_root = Path(req["legacy_dist_root"])
lib = _load_libkplus(dist_root)
stage = "resolve_port"
port_id = _find_port_id_with_kp(dist_root, req.get("port_id"), req.get("scan_index"))
timeout_ms = req.get("timeout_ms", 5000)
scpu_path = req["scpu_path"]
ncpu_path = req["ncpu_path"]
loader_path = req.get("loader_path") or str(Path(scpu_path).with_name("fw_loader.bin"))
scan_diag = _scan_diag_with_kp(dist_root)
detected_firmware = _firmware_from_scan(scan_diag, int(port_id))
selected_product_id = _product_id_from_scan(scan_diag, int(port_id))
diag = {
"selected_port_id": int(port_id),
"selected_product_id": (
f"0x{int(selected_product_id):04X}" if selected_product_id is not None else None
),
"timeout_ms": int(timeout_ms) if timeout_ms is not None else None,
"firmware_files": {
"loader": _file_diag(loader_path),
"scpu": _file_diag(scpu_path),
"ncpu": _file_diag(ncpu_path),
},
"scan": scan_diag,
"detected_firmware": detected_firmware,
}
stage = "connect"
device_group = _connect_with_magic(lib, port_id)
stage = "set_timeout"
if timeout_ms is not None:
lib.kp_set_timeout(device_group, int(timeout_ms))
method = ""
if detected_firmware == "KDP":
if not Path(loader_path).is_file():
raise RuntimeError(f"LOADER_NOT_FOUND: {loader_path}")
stage = "fw_switch_to_usb_boot_loader"
ret = lib.kp_update_kdp_firmware_from_files(
device_group,
loader_path.encode("utf-8"),
None,
True,
)
method = "kp_update_kdp_firmware_from_files(loader)->kp_load_firmware_from_file"
if ret != KP_SUCCESS:
stage = "disconnect_after_fw_fail"
lib.kp_disconnect_devices(device_group)
raise RuntimeError(
f"FW_LOAD_FAILED: method={method}, raw_code={ret}, msg={_errstr(lib, ret)}"
)
stage = "fw_load_kdp2_after_loader"
if timeout_ms is not None:
lib.kp_set_timeout(device_group, int(timeout_ms))
ret = lib.kp_load_firmware_from_file(
device_group,
scpu_path.encode("utf-8"),
ncpu_path.encode("utf-8"),
)
else:
stage = "fw_load_kdp2_direct"
method = "kp_load_firmware_from_file_direct"
ret = lib.kp_load_firmware_from_file(
device_group,
scpu_path.encode("utf-8"),
ncpu_path.encode("utf-8"),
)
if ret != KP_SUCCESS:
stage = "disconnect_after_fw_fail"
lib.kp_disconnect_devices(device_group)
raise RuntimeError(
f"FW_LOAD_FAILED: method={method}, raw_code={ret}, msg={_errstr(lib, ret)}"
)
stage = "disconnect_after_fw_success"
# After firmware update with auto_reboot, disconnect may fail due to USB re-enumeration.
disc = lib.kp_disconnect_devices(device_group)
if disc != KP_SUCCESS:
disc_info = f"disconnect_nonzero_raw={disc},signed={_normalize_code(disc)}"
else:
disc_info = "disconnect_ok"
print(
json.dumps(
{
"ok": True,
"port_id": int(port_id),
"connect_mode": "kp_connect_devices_with_magic_pass",
"firmware_method": method,
"disconnect_info": disc_info,
"diag": diag,
}
)
)
except Exception as exc:
print(json.dumps({"ok": False, "stage": stage, "error": str(exc), "diag": diag}))
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -1,8 +1,13 @@
from __future__ import annotations
import base64
import json
import os
import subprocess
import sys
import threading
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, List, Optional
from fastapi import FastAPI, HTTPException, Request
@ -13,6 +18,12 @@ import kp
SERVICE_VERSION = "0.1.0"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DFUT_ROOT = PROJECT_ROOT / "third_party" / "Kneron_DFUT"
DFUT_BIN = DFUT_ROOT / "bin"
DFUT_EXE = DFUT_BIN / "KneronDFUT.exe"
KP121_DIST = PROJECT_ROOT / "third_party" / "kneron_plus_1_2_1" / "dist"
KP121_RUNNER = Path(__file__).resolve().parent / "legacy_plus121_runner.py"
@dataclass
@ -45,37 +56,77 @@ def _require_device() -> kp.DeviceGroup:
def _image_format_from_str(value: str) -> kp.ImageFormat:
value = value.upper()
mapping = {
"RGB565": kp.ImageFormat.KP_IMAGE_FORMAT_RGB565,
"RGBA8888": kp.ImageFormat.KP_IMAGE_FORMAT_RGBA8888,
"RAW8": kp.ImageFormat.KP_IMAGE_FORMAT_RAW8,
"YUYV": kp.ImageFormat.KP_IMAGE_FORMAT_YUYV,
"YUV420": kp.ImageFormat.KP_IMAGE_FORMAT_YUV420,
# Build mapping from symbols that exist in the installed kp version.
candidate_names = {
"RGB565": "KP_IMAGE_FORMAT_RGB565",
"RGBA8888": "KP_IMAGE_FORMAT_RGBA8888",
"RAW8": "KP_IMAGE_FORMAT_RAW8",
"YUYV": "KP_IMAGE_FORMAT_YUYV",
"YUV420": "KP_IMAGE_FORMAT_YUV420",
}
mapping: Dict[str, kp.ImageFormat] = {}
for key, attr_name in candidate_names.items():
enum_value = getattr(kp.ImageFormat, attr_name, None)
if enum_value is not None:
mapping[key] = enum_value
if value not in mapping:
supported = ", ".join(sorted(mapping.keys()))
raise HTTPException(
status_code=400,
detail=_err("INVALID_IMAGE_FORMAT", f"Unsupported image_format: {value}"),
detail=_err(
"INVALID_IMAGE_FORMAT",
f"Unsupported image_format: {value}. supported=[{supported}]",
),
)
return mapping[value]
def _channels_ordering_from_str(value: str) -> kp.ChannelOrdering:
value = value.upper()
mapping = {
"HCW": kp.ChannelOrdering.KP_CHANNEL_ORDERING_HCW,
"CHW": kp.ChannelOrdering.KP_CHANNEL_ORDERING_CHW,
"HWC": kp.ChannelOrdering.KP_CHANNEL_ORDERING_HWC,
"DEFAULT": kp.ChannelOrdering.KP_CHANNEL_ORDERING_DEFAULT,
candidate_names = {
"HCW": "KP_CHANNEL_ORDERING_HCW",
"CHW": "KP_CHANNEL_ORDERING_CHW",
"HWC": "KP_CHANNEL_ORDERING_HWC",
"DEFAULT": "KP_CHANNEL_ORDERING_DEFAULT",
}
mapping: Dict[str, kp.ChannelOrdering] = {}
for key, attr_name in candidate_names.items():
enum_value = getattr(kp.ChannelOrdering, attr_name, None)
if enum_value is not None:
mapping[key] = enum_value
if "DEFAULT" not in mapping:
if "CHW" in mapping:
mapping["DEFAULT"] = mapping["CHW"]
elif "HCW" in mapping:
mapping["DEFAULT"] = mapping["HCW"]
if value not in mapping:
supported = ", ".join(sorted(mapping.keys()))
raise HTTPException(
status_code=400,
detail=_err("INVALID_CHANNEL_ORDERING", f"Unsupported channels_ordering: {value}"),
detail=_err(
"INVALID_CHANNEL_ORDERING",
f"Unsupported channels_ordering: {value}. supported=[{supported}]",
),
)
return mapping[value]
def _expected_image_size_bytes(image_format: str, width: int, height: int) -> Optional[int]:
fmt = image_format.upper()
if fmt == "RGB565":
return width * height * 2
if fmt == "RGBA8888":
return width * height * 4
if fmt == "RAW8":
return width * height
if fmt == "YUYV":
return width * height * 2
if fmt == "YUV420":
# YUV420 requires even width/height; caller checks exact size only.
return (width * height * 3) // 2
return None
def _product_name_from_id(product_id: int) -> str:
try:
return kp.ProductId(product_id).name.replace("KP_DEVICE_", "")
@ -112,10 +163,24 @@ class FirmwareLoadRequest(BaseModel):
ncpu_path: str
class LegacyPlusFirmwareLoadRequest(BaseModel):
port_id: Optional[int] = Field(default=None)
scan_index: Optional[int] = Field(default=None)
timeout_ms: Optional[int] = Field(default=5000)
loader_path: Optional[str] = Field(default=None)
scpu_path: str
ncpu_path: str
class ModelLoadRequest(BaseModel):
nef_path: str
class DriverInstallRequest(BaseModel):
target: str = "ALL" # ALL | KL520 | KL720 | KL630 | KL730 | KL830
force: bool = False
class InferenceRunRequest(BaseModel):
model_id: int
image_format: str
@ -126,6 +191,316 @@ class InferenceRunRequest(BaseModel):
output_dtype: str = "float32"
def _resolve_port_id(req: ConnectRequest) -> int:
port_id = req.port_id
if port_id is not None:
return int(port_id)
device_list = kp.core.scan_devices()
if device_list.device_descriptor_number == 0:
raise HTTPException(status_code=404, detail=_err("NO_DEVICE", "No device found"))
scan_index = 0 if req.scan_index is None else req.scan_index
if scan_index < 0 or scan_index >= device_list.device_descriptor_number:
raise HTTPException(
status_code=400,
detail=_err("INVALID_SCAN_INDEX", f"Invalid scan_index: {scan_index}"),
)
return int(device_list.device_descriptor_list[scan_index].usb_port_id)
def _scan_devices_snapshot() -> List[Dict[str, Any]]:
device_list = kp.core.scan_devices()
out: List[Dict[str, Any]] = []
for i in range(int(device_list.device_descriptor_number)):
d = device_list.device_descriptor_list[i]
out.append(
{
"scan_index": i,
"usb_port_id": int(d.usb_port_id),
"product_id": int(d.product_id),
"firmware": str(d.firmware),
"usb_port_path": str(d.usb_port_path),
}
)
return out
def _kl520_kdp_observed_after_timeout(target_port_id: int) -> Dict[str, Any]:
devices = _scan_devices_snapshot()
kl520 = [d for d in devices if int(d["product_id"]) == 0x0100]
exact = [d for d in kl520 if int(d["usb_port_id"]) == int(target_port_id)]
exact_kdp = [d for d in exact if "KDP" in str(d["firmware"]).upper()]
if exact_kdp:
return {"is_success": True, "reason": "exact_port_kdp", "devices": devices}
if not exact:
any_kdp = [d for d in kl520 if "KDP" in str(d["firmware"]).upper()]
if any_kdp:
return {"is_success": True, "reason": "reenumerated_port_kdp", "devices": devices}
return {"is_success": False, "reason": "kdp_not_observed", "devices": devices}
def _ensure_file_exists(path: Path, label: str) -> None:
if not path.is_file():
raise HTTPException(
status_code=500,
detail=_err("FILE_NOT_FOUND", f"{label} not found: {path}"),
)
def _run_dfut_command(
args: List[str],
timeout_sec: int = 180,
echo_console: bool = False,
echo_tag: str = "DFUT",
) -> Dict[str, Any]:
_ensure_file_exists(DFUT_EXE, "DFUT executable")
if echo_console:
print(f"[{echo_tag}] CMD: {' '.join(args)}", flush=True)
stdout_lines: List[str] = []
stderr_lines: List[str] = []
proc = subprocess.Popen(
args,
cwd=str(DFUT_BIN),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
)
def _read_stream(stream: Any, collector: List[str], stream_name: str) -> None:
if stream is None:
return
try:
for line in iter(stream.readline, ""):
text = line.rstrip("\r\n")
collector.append(text)
if echo_console:
print(f"[{echo_tag}][{stream_name}] {text}", flush=True)
finally:
try:
stream.close()
except Exception:
pass
t_out = threading.Thread(target=_read_stream, args=(proc.stdout, stdout_lines, "STDOUT"), daemon=True)
t_err = threading.Thread(target=_read_stream, args=(proc.stderr, stderr_lines, "STDERR"), daemon=True)
t_out.start()
t_err.start()
try:
proc.wait(timeout=timeout_sec)
except subprocess.TimeoutExpired as exc:
proc.kill()
proc.wait()
t_out.join(timeout=1.0)
t_err.join(timeout=1.0)
stdout_text = "\n".join(stdout_lines).strip()
stderr_text = "\n".join(stderr_lines).strip()
raise HTTPException(
status_code=504,
detail=_err(
"DFUT_TIMEOUT",
(
f"DFUT timed out after {timeout_sec}s. "
f"stdout={stdout_text} "
f"stderr={stderr_text}"
),
),
)
t_out.join(timeout=1.0)
t_err.join(timeout=1.0)
stdout_text = "\n".join(stdout_lines).strip()
stderr_text = "\n".join(stderr_lines).strip()
if proc.returncode != 0:
signed_code = proc.returncode
if signed_code > 0x7FFFFFFF:
signed_code -= 0x100000000
raise HTTPException(
status_code=500,
detail=_err(
"DFUT_FAILED",
(
f"DFUT failed with return code {proc.returncode} (signed={signed_code}). "
f"command={args}. "
f"stdout={stdout_text} "
f"stderr={stderr_text}"
),
),
)
return {
"return_code": proc.returncode,
"stdout": stdout_text,
"stderr": stderr_text,
"command": args,
}
def _run_legacy_plus121_load_firmware(req: LegacyPlusFirmwareLoadRequest) -> Dict[str, Any]:
_ensure_file_exists(KP121_RUNNER, "Legacy 1.2.1 runner")
if not KP121_DIST.is_dir():
raise HTTPException(
status_code=500,
detail=_err(
"LEGACY_PLUS_NOT_FOUND",
f"Legacy KneronPLUS 1.2.1 package directory not found: {KP121_DIST}",
),
)
payload = {
"port_id": req.port_id,
"scan_index": req.scan_index,
"timeout_ms": req.timeout_ms,
"loader_path": req.loader_path,
"scpu_path": req.scpu_path,
"ncpu_path": req.ncpu_path,
"legacy_dist_root": str(KP121_DIST),
}
env = os.environ.copy()
existing_pythonpath = env.get("PYTHONPATH", "")
legacy_pythonpath = str(KP121_DIST)
env["PYTHONPATH"] = (
f"{legacy_pythonpath}{os.pathsep}{existing_pythonpath}"
if existing_pythonpath
else legacy_pythonpath
)
proc = subprocess.run(
[sys.executable, str(KP121_RUNNER), json.dumps(payload)],
cwd=str(PROJECT_ROOT),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
env=env,
)
if proc.returncode != 0:
runner_json = None
if proc.stdout.strip():
try:
runner_json = json.loads(proc.stdout.strip().splitlines()[-1])
except json.JSONDecodeError:
runner_json = None
debug_payload = {
"return_code": proc.returncode,
"stdout": proc.stdout.strip(),
"stderr": proc.stderr.strip(),
"runner_json": runner_json,
}
print("[legacy-plus121-runner-failed]", json.dumps(debug_payload, ensure_ascii=False))
raise HTTPException(
status_code=500,
detail=_err(
"LEGACY_PLUS_FW_LOAD_FAILED",
json.dumps(debug_payload, ensure_ascii=False),
),
)
parsed = {}
if proc.stdout.strip():
try:
parsed = json.loads(proc.stdout.strip().splitlines()[-1])
except json.JSONDecodeError:
parsed = {"raw_stdout": proc.stdout.strip()}
return {
"return_code": proc.returncode,
"result": parsed,
}
def _target_product_ids(target: str) -> List[kp.ProductId]:
target = target.upper()
if target == "KL520":
return [kp.ProductId.KP_DEVICE_KL520]
if target == "KL720":
return [kp.ProductId.KP_DEVICE_KL720_LEGACY, kp.ProductId.KP_DEVICE_KL720]
if target == "KL630":
return [kp.ProductId.KP_DEVICE_KL630]
if target == "KL730":
return [kp.ProductId.KP_DEVICE_KL730]
if target == "KL830":
return [kp.ProductId.KP_DEVICE_KL830]
if target == "ALL":
return [
kp.ProductId.KP_DEVICE_KL520,
kp.ProductId.KP_DEVICE_KL720_LEGACY,
kp.ProductId.KP_DEVICE_KL720,
kp.ProductId.KP_DEVICE_KL630,
kp.ProductId.KP_DEVICE_KL730,
kp.ProductId.KP_DEVICE_KL830,
]
raise HTTPException(status_code=400, detail=_err("INVALID_TARGET", f"Unsupported target: {target}"))
def _pid_to_product_name(pid_value: int) -> str:
try:
return kp.ProductId(pid_value).name.replace("KP_DEVICE_", "")
except ValueError:
return f"UNKNOWN_0x{pid_value:04X}"
def _query_windows_driver_status() -> List[Dict[str, Any]]:
# Query current connected Kneron USB devices from Windows PnP layer.
ps = (
"$items = Get-CimInstance Win32_PnPEntity | "
"Where-Object { $_.PNPDeviceID -like 'USB\\VID_3231&PID_*' } | "
"Select-Object PNPDeviceID,Name,Service,Status; "
"if ($null -eq $items) { '[]' } else { $items | ConvertTo-Json -Depth 3 }"
)
proc = subprocess.run(
["powershell", "-NoProfile", "-Command", ps],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
if proc.returncode != 0:
raise HTTPException(
status_code=500,
detail=_err("DRIVER_CHECK_FAILED", proc.stderr.strip() or "failed to query Windows PnP devices"),
)
out = proc.stdout.strip()
if not out:
return []
try:
parsed = json.loads(out)
except json.JSONDecodeError:
return []
if isinstance(parsed, dict):
parsed = [parsed]
results = []
for item in parsed:
pnp_id = item.get("PNPDeviceID", "")
pid_hex = ""
if "PID_" in pnp_id:
pid_hex = pnp_id.split("PID_")[1][:4]
pid_val = int(pid_hex, 16) if pid_hex else None
service = item.get("Service") or ""
results.append(
{
"pnp_device_id": pnp_id,
"name": item.get("Name"),
"status": item.get("Status"),
"service": service,
"pid_hex": f"0x{pid_hex}" if pid_hex else None,
"pid_value": pid_val,
"product_name": _pid_to_product_name(pid_val) if pid_val is not None else "UNKNOWN",
"is_winusb": service.lower() == "winusb",
}
)
return results
@app.get("/health")
def health() -> Dict[str, Any]:
return _ok({"status": "up"})
@ -175,21 +550,7 @@ def connect(req: ConnectRequest) -> Dict[str, Any]:
STATE.port_id = None
STATE.model_desc = None
port_id = req.port_id
if port_id is None:
device_list = kp.core.scan_devices()
if device_list.device_descriptor_number == 0:
raise HTTPException(status_code=404, detail=_err("NO_DEVICE", "No device found"))
if req.scan_index is None:
scan_index = 0
else:
scan_index = req.scan_index
if scan_index < 0 or scan_index >= device_list.device_descriptor_number:
raise HTTPException(
status_code=400,
detail=_err("INVALID_SCAN_INDEX", f"Invalid scan_index: {scan_index}"),
)
port_id = device_list.device_descriptor_list[scan_index].usb_port_id
port_id = _resolve_port_id(req)
try:
device_group = kp.core.connect_devices([int(port_id)])
@ -206,6 +567,35 @@ def connect(req: ConnectRequest) -> Dict[str, Any]:
return _ok({"connected": True, "port_id": STATE.port_id})
@app.post("/devices/connect_force")
def connect_force(req: ConnectRequest) -> Dict[str, Any]:
with STATE_LOCK:
if STATE.device_group is not None:
try:
kp.core.disconnect_devices(STATE.device_group)
except kp.ApiKPException:
pass
STATE.device_group = None
STATE.port_id = None
STATE.model_desc = None
port_id = _resolve_port_id(req)
try:
device_group = kp.core.connect_devices_without_check([int(port_id)])
if req.timeout_ms is not None:
kp.core.set_timeout(device_group, int(req.timeout_ms))
except kp.ApiKPException as exc:
raise HTTPException(
status_code=500,
detail=_err(str(exc.api_return_code), str(exc)),
)
STATE.device_group = device_group
STATE.port_id = int(port_id)
return _ok({"connected": True, "port_id": STATE.port_id, "forced": True})
@app.post("/devices/disconnect")
def disconnect() -> Dict[str, Any]:
with STATE_LOCK:
@ -223,6 +613,70 @@ def disconnect() -> Dict[str, Any]:
return _ok({"connected": False})
@app.get("/driver/check")
def driver_check() -> Dict[str, Any]:
entries = _query_windows_driver_status()
all_winusb = all(entry["is_winusb"] for entry in entries) if entries else False
return _ok(
{
"entries": entries,
"all_connected_kneron_are_winusb": all_winusb,
"count": len(entries),
"note": "Run service as Administrator if driver install/update fails.",
}
)
@app.post("/driver/install")
def driver_install(req: DriverInstallRequest) -> Dict[str, Any]:
targets = _target_product_ids(req.target)
results = []
for product_id in targets:
try:
kp.core.install_driver_for_windows(product_id=product_id)
results.append({"product_id": f"0x{product_id.value:04X}", "product_name": product_id.name, "installed": True})
except kp.ApiKPException as exc:
results.append(
{
"product_id": f"0x{product_id.value:04X}",
"product_name": product_id.name,
"installed": False,
"error_code": str(exc.api_return_code),
"error_message": str(exc),
}
)
return _ok({"target": req.target.upper(), "results": results})
@app.post("/driver/ensure")
def driver_ensure(req: DriverInstallRequest) -> Dict[str, Any]:
target = req.target.upper()
target_ids = _target_product_ids(target)
target_pid_values = {pid.value for pid in target_ids}
entries = _query_windows_driver_status()
needs_install = req.force
for entry in entries:
if entry["pid_value"] in target_pid_values and not entry["is_winusb"]:
needs_install = True
break
install_result = None
if needs_install:
install_result = driver_install(req)
after = _query_windows_driver_status()
return _ok(
{
"target": target,
"needed_install": needs_install,
"install_result": install_result["data"] if install_result else None,
"before": entries,
"after": after,
}
)
@app.post("/firmware/load")
def firmware_load(req: FirmwareLoadRequest) -> Dict[str, Any]:
device_group = _require_device()
@ -236,6 +690,145 @@ def firmware_load(req: FirmwareLoadRequest) -> Dict[str, Any]:
return _ok({"loaded": True})
@app.post("/firmware/legacy-plus121/load")
def firmware_legacy_plus121_load(req: LegacyPlusFirmwareLoadRequest) -> Dict[str, Any]:
# Ensure current process does not hold USB handles before legacy subprocess runs.
with STATE_LOCK:
if STATE.device_group is not None:
try:
kp.core.disconnect_devices(STATE.device_group)
except kp.ApiKPException:
pass
STATE.device_group = None
STATE.port_id = None
STATE.model_desc = None
_ensure_file_exists(Path(req.scpu_path), "Legacy SCPU firmware")
_ensure_file_exists(Path(req.ncpu_path), "Legacy NCPU firmware")
if req.loader_path is not None:
_ensure_file_exists(Path(req.loader_path), "Legacy loader firmware")
result = _run_legacy_plus121_load_firmware(req)
return _ok({"loaded": True, "legacy_plus_version": "1.2.1", "runner": result})
@app.post("/firmware/legacy-upgrade/kl520")
def firmware_legacy_upgrade_kl520(req: ConnectRequest) -> Dict[str, Any]:
print("[DFUT-KL520-UPGRADE] endpoint entered", flush=True)
port_id = _resolve_port_id(req)
print(f"[DFUT-KL520-UPGRADE] resolved port_id={port_id}", flush=True)
fw_scpu = PROJECT_ROOT / "firmware" / "KL520" / "fw_scpu.bin"
fw_ncpu = PROJECT_ROOT / "firmware" / "KL520" / "fw_ncpu.bin"
_ensure_file_exists(fw_scpu, "KL520 SCPU firmware")
_ensure_file_exists(fw_ncpu, "KL520 NCPU firmware")
args = [
str(DFUT_EXE),
"--kl520-flash-boot",
"--port",
str(port_id),
"--scpu",
str(fw_scpu),
"--ncpu",
str(fw_ncpu),
"--quiet",
]
result = _run_dfut_command(args, echo_console=True, echo_tag="DFUT-KL520-UPGRADE")
return _ok(
{
"upgraded": True,
"target": "KL520",
"port_id": port_id,
"dfut": result,
}
)
@app.post("/firmware/legacy-downgrade/kl520")
def firmware_legacy_downgrade_kl520(req: ConnectRequest) -> Dict[str, Any]:
port_id = _resolve_port_id(req)
fw_scpu = PROJECT_ROOT / "firmware" / "KL520_kdp" / "fw_scpu.bin"
fw_ncpu = PROJECT_ROOT / "firmware" / "KL520_kdp" / "fw_ncpu.bin"
_ensure_file_exists(fw_scpu, "KL520 KDP SCPU firmware")
_ensure_file_exists(fw_ncpu, "KL520 KDP NCPU firmware")
args = [
str(DFUT_EXE),
"--kl520-update",
"--port",
str(port_id),
"--scpu",
str(fw_scpu),
"--ncpu",
str(fw_ncpu),
]
timeout_sec = 30
try:
result = _run_dfut_command(
args,
timeout_sec=timeout_sec,
echo_console=False,
echo_tag="DFUT-KL520-DOWNGRADE",
)
except HTTPException as exc:
detail = exc.detail if isinstance(exc.detail, dict) else None
err_code = (
str(detail.get("error", {}).get("code", "")) if isinstance(detail, dict) else ""
)
if exc.status_code == 504 and err_code == "DFUT_TIMEOUT":
post = _kl520_kdp_observed_after_timeout(port_id)
if post["is_success"]:
return _ok(
{
"downgraded": True,
"target": "KL520",
"port_id": port_id,
"dfut_timeout_but_verified": True,
"verification_reason": post["reason"],
"scan_after_timeout": post["devices"],
"warning": "DFUT timeout but KL520 KDP state observed after timeout.",
}
)
raise
return _ok(
{
"downgraded": True,
"target": "KL520",
"port_id": port_id,
"dfut": result,
}
)
@app.post("/firmware/legacy-upgrade/kl720")
def firmware_legacy_upgrade_kl720(req: ConnectRequest) -> Dict[str, Any]:
port_id = _resolve_port_id(req)
fw_scpu = PROJECT_ROOT / "firmware" / "KL720" / "fw_scpu.bin"
fw_ncpu = PROJECT_ROOT / "firmware" / "KL720" / "fw_ncpu.bin"
_ensure_file_exists(fw_scpu, "KL720 SCPU firmware")
_ensure_file_exists(fw_ncpu, "KL720 NCPU firmware")
args = [
str(DFUT_EXE),
"--kl720-update",
"--port",
str(port_id),
"--scpu",
str(fw_scpu),
"--ncpu",
str(fw_ncpu),
"--quiet",
]
result = _run_dfut_command(args)
return _ok(
{
"upgraded": True,
"target": "KL720",
"port_id": port_id,
"dfut": result,
}
)
@app.post("/models/load")
def models_load(req: ModelLoadRequest) -> Dict[str, Any]:
device_group = _require_device()
@ -321,14 +914,31 @@ def inference_run(req: InferenceRunRequest) -> Dict[str, Any]:
detail=_err(str(exc.api_return_code), str(exc)),
)
b64_text = req.image_base64.strip()
if b64_text.startswith("data:") and "," in b64_text:
b64_text = b64_text.split(",", 1)[1]
try:
image_bytes = base64.b64decode(req.image_base64)
image_bytes = base64.b64decode(b64_text)
except (ValueError, TypeError):
raise HTTPException(
status_code=400,
detail=_err("INVALID_BASE64", "image_base64 is not valid base64 data"),
)
expected_size = _expected_image_size_bytes(req.image_format, req.width, req.height)
if expected_size is not None and len(image_bytes) != expected_size:
raise HTTPException(
status_code=400,
detail=_err(
"INVALID_IMAGE_SIZE",
(
f"image bytes size mismatch: expected={expected_size}, actual={len(image_bytes)}. "
"Send raw pixel bytes for selected image_format (not BMP/JPEG/PNG file bytes)."
),
),
)
input_image = kp.GenericInputNodeImage(
image=image_bytes,
width=req.width,

Binary file not shown.

View File

@ -28,6 +28,15 @@ cd local_service_win
python -m pip install --force-reinstall .\KneronPLUS-3.1.2-py3-none-any.whl
```
## Dependency Strategy
- Open-source packages installed by pip:
- `fastapi`, `uvicorn`, `numpy`, `PyQt5`, `opencv-python`, `pyinstaller`, `pyarmor`
- Non-pip dependency:
- `KneronPLUS` (installed from local wheel)
- Bundled runtime (not pip):
- `third_party/Kneron_DFUT` is copied into this repo and used by LocalAPI to recover old firmware in one tool.
- `third_party/kneron_plus_1_2_1/dist` is extracted from `KneronPLUS-1.2.1` wheel and used by a subprocess runner for old-device firmware update experiments.
## Cross-Project Workflow
This repo is the main PoC implementation. If additional references are required, we can switch to
other repos during the same conversation and return here as needed. This is workable.
@ -124,12 +133,75 @@ Response
}
```
### `POST /devices/connect_force`
Notes
- Force connection without firmware validation.
- Use this when firmware is incompatible and you need to call `/firmware/load` first.
Request
```json
{ "port_id": 32 }
```
Response
```json
{
"ok": true,
"data": {
"connected": true,
"port_id": 32,
"forced": true
},
"error": null
}
```
### `POST /devices/disconnect`
Response
```json
{ "ok": true, "data": { "connected": false }, "error": null }
```
### `GET /driver/check`
Notes
- Query currently connected Kneron USB devices from Windows PnP.
- Reports whether each entry is bound to WinUSB.
Response
```json
{
"ok": true,
"data": {
"entries": [
{
"pnp_device_id": "USB\\VID_3231&PID_0720\\...",
"service": "WinUSB",
"pid_hex": "0x0720",
"product_name": "KL720",
"is_winusb": true
}
],
"all_connected_kneron_are_winusb": true
},
"error": null
}
```
### `POST /driver/install`
Notes
- Install/replace driver using `kp.core.install_driver_for_windows`.
- Requires Administrator privilege on Windows.
Request
```json
{ "target": "KL720", "force": false }
```
### `POST /driver/ensure`
Notes
- Check connected device driver binding, auto install if not WinUSB (or `force=true`).
- `target`: `ALL` | `KL520` | `KL720` | `KL630` | `KL730` | `KL830`
Request
```json
{ "target": "ALL", "force": false }
```
### `POST /firmware/load`
Request
```json
@ -143,6 +215,80 @@ Response
{ "ok": true, "data": { "loaded": true }, "error": null }
```
### `POST /firmware/legacy-plus121/load`
Notes
- Experimental endpoint for old hardware/firmware path.
- Independent route from DFUT.
- Runs a subprocess with bundled `KneronPLUS 1.2.1` package and calls `libkplus.dll` directly by `ctypes`.
- Single-endpoint auto flow:
- Scan target device firmware state.
- If firmware is `KDP`: first call loader (`fw_loader.bin`) to switch USB-boot, then call `kp_load_firmware_from_file(scpu,ncpu)`.
- If firmware is not `KDP`: call `kp_load_firmware_from_file(scpu,ncpu)` directly.
- Finally disconnect.
- Diagnostics include selected port, detected firmware, scan snapshot, and firmware file metadata (path/size/sha256).
Request
```json
{
"port_id": 32,
"loader_path": "C:\\path\\fw_loader.bin",
"scpu_path": "C:\\path\\fw_scpu.bin",
"ncpu_path": "C:\\path\\fw_ncpu.bin"
}
```
Response
```json
{
"ok": true,
"data": {
"loaded": true,
"legacy_plus_version": "1.2.1"
},
"error": null
}
```
### `POST /firmware/legacy-upgrade/kl520`
Notes
- Used for old KL520 firmware recovery path.
- Runs bundled DFUT console with `--kl520-update`.
Request
```json
{ "port_id": 32 }
```
Response
```json
{
"ok": true,
"data": {
"upgraded": true,
"target": "KL520",
"port_id": 32
},
"error": null
}
```
### `POST /firmware/legacy-upgrade/kl720`
Notes
- Used for old KL720 / KL720 legacy recovery path.
- Runs bundled DFUT console with `--kl720-update`.
Request
```json
{ "port_id": 32 }
```
Response
```json
{
"ok": true,
"data": {
"upgraded": true,
"target": "KL720",
"port_id": 32
},
"error": null
}
```
### `POST /models/load`
Request
```json
@ -246,21 +392,36 @@ Message (server -> client)
- Flash model update / flash firmware update may not be exposed in Python.
- Use C library or request Kneron to expose in wrapper if required.
- Browser security model prevents direct USB access; local service is required.
- Driver install/update on Windows may require Administrator privileges (`install_driver_for_windows` can fail without elevation).
- MEMO: define production approach for privilege handling (installer-time elevation, helper process with UAC prompt, or enterprise pre-install policy) so end-user flow does not get blocked.
## API Test Progress (Windows PoC)
Updated: 2026-03-03
### Completed
- `GET /health`
- `GET /version`
- `GET /devices`
- `POST /devices/connect`
- `POST /devices/connect_force`
- `POST /devices/disconnect`
### Pending
- `GET /driver/check`
- `POST /driver/install`
- `POST /driver/ensure`
- `POST /firmware/load`
- `POST /firmware/legacy-plus121/load`
- `POST /firmware/legacy-upgrade/kl520`
- `POST /firmware/legacy-downgrade/kl520`
- `POST /firmware/legacy-upgrade/kl720`
- `POST /models/load`
- `POST /models/clear`
- `POST /models/reset`
- `POST /inference/run`
### Pending
- None (for currently implemented HTTP endpoints).
### Not Implemented Yet (API spec)
- `WS /ws`
- `POST /firmware/update`
@ -296,3 +457,28 @@ Message (server -> client)
- Decide service framework (FastAPI/Flask).
- Define error model and device state machine.
- Plan installer workflow (driver + service).
## Legacy Firmware Story And Recommended Handling
- Background:
- Many shipped devices are still on old KDP firmware or KL720 legacy states.
- In that state, `kp.core.connect_devices` and even `connect_devices_without_check` may still return `KP_ERROR_INVALID_FIRMWARE_24`.
- Goal:
- Keep user operations in one tool without requiring a separate DFUT install flow.
- Recommended handling:
1. User scans devices via `GET /devices`.
2. If normal connect fails with `KP_ERROR_INVALID_FIRMWARE_24`, call:
- `POST /firmware/legacy-upgrade/kl520` or
- `POST /firmware/legacy-upgrade/kl720`
3. Re-scan and reconnect using `POST /devices/connect`.
4. Continue with `POST /firmware/load` (if needed), `POST /models/load`, and inference.
- Experimental alternative:
- If DFUT route is blocked on specific old-device states, test `POST /firmware/legacy-plus121/load` as an independent non-DFUT legacy SDK compatibility path.
- Notes:
- Recovery endpoints use bundled `third_party/Kneron_DFUT/bin/KneronDFUT.exe`.
- This keeps firmware recovery and inference service in the same product boundary.
## Validation Memo (Next)
- Record and verify on newer KneronPLUS versions:
- For KL520 old KDP state, `loader -> load_firmware_from_file(scpu,ncpu)` sequence works in legacy-plus121 path.
- Hypothesis: the same sequence may also work on newer PLUS runtime.
- Action later: add an explicit validation task on current PLUS branch and capture pass/fail matrix by device FW state.

View File

@ -0,0 +1,271 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>BMP to RGB565</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 16px;
background: #f7f7f7;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.controls {
margin: 12px 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
textarea {
width: 100%;
min-height: 140px;
padding: 12px;
box-sizing: border-box;
font-family: Consolas, monospace;
}
img {
max-width: 100%;
border: 1px solid #ddd;
background: #fff;
display: block;
margin: 8px 0;
}
.hint {
color: #555;
font-size: 13px;
}
.meta {
margin: 8px 0;
font-family: Consolas, monospace;
background: #fff;
border: 1px solid #ddd;
padding: 8px;
}
.row {
margin-top: 10px;
}
button {
padding: 6px 10px;
}
label {
font-size: 13px;
}
input[type="number"] {
width: 120px;
}
</style>
</head>
<body>
<div class="container">
<h1>BMP to RGB565 (Raw)</h1>
<p class="hint">
Select a BMP file, convert pixels to RGB565 raw bytes (little-endian), then copy Base64 for
<code>/inference/run</code>.
</p>
<div class="controls">
<input id="fileInput" type="file" accept=".bmp,image/bmp" />
<label>Model Preset:</label>
<select id="modelPreset">
<option value="tiny_yolo">TinyYOLO (KL520)</option>
<option value="yolov5">YOLOv5 (KL520/KL720)</option>
<option value="fcos">FCOS (KL520/KL720)</option>
</select>
<label>Target W:</label>
<input id="targetWidth" type="number" min="1" placeholder="original" />
<label>Target H:</label>
<input id="targetHeight" type="number" min="1" placeholder="original" />
<button id="convertBtn">Convert</button>
<button id="clearBtn">Clear</button>
</div>
<img id="preview" alt="Preview will appear here" />
<div class="meta" id="meta">No file loaded.</div>
<div class="row">
<label for="base64Output">RGB565 Base64 (raw bytes)</label>
<textarea id="base64Output" placeholder="RGB565 base64 output..."></textarea>
</div>
<div class="controls">
<label>model_id:</label>
<input id="modelId" type="number" value="19" />
<button id="copyB64Btn">Copy Base64</button>
<button id="copyPayloadBtn">Copy Payload JSON</button>
</div>
<div class="row">
<label for="payloadOutput">Payload sample</label>
<textarea id="payloadOutput" placeholder="Payload JSON..."></textarea>
</div>
</div>
<script>
const fileInput = document.getElementById("fileInput");
const convertBtn = document.getElementById("convertBtn");
const clearBtn = document.getElementById("clearBtn");
const copyB64Btn = document.getElementById("copyB64Btn");
const copyPayloadBtn = document.getElementById("copyPayloadBtn");
const modelIdInput = document.getElementById("modelId");
const modelPresetSelect = document.getElementById("modelPreset");
const targetWidthInput = document.getElementById("targetWidth");
const targetHeightInput = document.getElementById("targetHeight");
const preview = document.getElementById("preview");
const meta = document.getElementById("meta");
const base64Output = document.getElementById("base64Output");
const payloadOutput = document.getElementById("payloadOutput");
const modelPresets = {
tiny_yolo: { modelId: 19, width: 224, height: 224, label: "TinyYOLO (KL520)" },
yolov5: { modelId: 20005, width: 640, height: 640, label: "YOLOv5" },
fcos: { modelId: 20004, width: 512, height: 512, label: "FCOS" }
};
let currentWidth = 0;
let currentHeight = 0;
function toBase64(uint8Array) {
let binary = "";
const chunkSize = 0x8000;
for (let i = 0; i < uint8Array.length; i += chunkSize) {
const chunk = uint8Array.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
return btoa(binary);
}
function rgbTo565(r, g, b) {
// 5 bits red, 6 bits green, 5 bits blue
const r5 = (r >> 3) & 0x1f;
const g6 = (g >> 2) & 0x3f;
const b5 = (b >> 3) & 0x1f;
return (r5 << 11) | (g6 << 5) | b5;
}
function buildPayload(base64Value, width, height) {
const payload = {
model_id: Number(modelIdInput.value || 0),
image_format: "RGB565",
width: width,
height: height,
image_base64: base64Value,
channels_ordering: "DEFAULT",
output_dtype: "float32"
};
payloadOutput.value = JSON.stringify(payload, null, 2);
}
function setMeta(message) {
meta.textContent = message;
}
function applyModelPreset(presetKey) {
const preset = modelPresets[presetKey];
if (!preset) return;
modelIdInput.value = String(preset.modelId);
targetWidthInput.value = String(preset.width);
targetHeightInput.value = String(preset.height);
setMeta(
`preset=${preset.label}, model_id=${preset.modelId}, target=${preset.width}x${preset.height}`
);
if (base64Output.value && currentWidth > 0 && currentHeight > 0) {
buildPayload(base64Output.value, currentWidth, currentHeight);
}
}
async function convert() {
const file = fileInput.files && fileInput.files[0];
if (!file) {
setMeta("Please select a BMP file first.");
return;
}
const objectUrl = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
const srcWidth = img.width;
const srcHeight = img.height;
const tw = Number(targetWidthInput.value);
const th = Number(targetHeightInput.value);
const hasTarget = Number.isFinite(tw) && Number.isFinite(th) && tw > 0 && th > 0;
currentWidth = hasTarget ? Math.floor(tw) : srcWidth;
currentHeight = hasTarget ? Math.floor(th) : srcHeight;
const canvas = document.createElement("canvas");
canvas.width = currentWidth;
canvas.height = currentHeight;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, currentWidth, currentHeight);
const imageData = ctx.getImageData(0, 0, currentWidth, currentHeight).data;
const out = new Uint8Array(currentWidth * currentHeight * 2);
let p = 0;
for (let i = 0; i < imageData.length; i += 4) {
const r = imageData[i];
const g = imageData[i + 1];
const b = imageData[i + 2];
const v565 = rgbTo565(r, g, b);
// little-endian bytes
out[p++] = v565 & 0xff;
out[p++] = (v565 >> 8) & 0xff;
}
const b64 = toBase64(out);
base64Output.value = b64;
buildPayload(b64, currentWidth, currentHeight);
preview.src = objectUrl;
setMeta(
`file=${file.name}, src=${srcWidth}x${srcHeight}, out=${currentWidth}x${currentHeight}, rgb565_bytes=${out.length}, expected=${
currentWidth * currentHeight * 2
}`
);
};
img.onerror = () => {
setMeta("Failed to decode image. Ensure the file is a valid BMP.");
URL.revokeObjectURL(objectUrl);
};
img.src = objectUrl;
}
function clearAll() {
fileInput.value = "";
base64Output.value = "";
payloadOutput.value = "";
preview.removeAttribute("src");
currentWidth = 0;
currentHeight = 0;
setMeta("No file loaded.");
}
convertBtn.addEventListener("click", convert);
clearBtn.addEventListener("click", clearAll);
modelPresetSelect.addEventListener("change", () => {
applyModelPreset(modelPresetSelect.value);
});
modelIdInput.addEventListener("change", () => {
if (base64Output.value && currentWidth > 0 && currentHeight > 0) {
buildPayload(base64Output.value, currentWidth, currentHeight);
}
});
copyB64Btn.addEventListener("click", async () => {
if (!base64Output.value) return;
await navigator.clipboard.writeText(base64Output.value);
});
copyPayloadBtn.addEventListener("click", async () => {
if (!payloadOutput.value) return;
await navigator.clipboard.writeText(payloadOutput.value);
});
applyModelPreset(modelPresetSelect.value);
</script>
</body>
</html>

View File

@ -0,0 +1,955 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Payload Detection Viewer (YOLO/TinyYOLO/FCOS)</title>
<style>
:root {
--bg: #f5f6f8;
--card: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--line: #d1d5db;
--accent: #2563eb;
}
body {
margin: 0;
background: var(--bg);
color: var(--text);
font-family: Arial, sans-serif;
}
.wrap {
max-width: 1300px;
margin: 20px auto;
padding: 0 12px 24px;
}
h1 {
margin: 0 0 8px;
}
.hint {
color: var(--muted);
font-size: 13px;
margin: 0 0 12px;
}
.panel {
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
}
textarea {
width: 100%;
min-height: 200px;
box-sizing: border-box;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
font-family: Consolas, monospace;
}
.row {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.row label {
font-size: 13px;
}
input[type="number"], input[type="text"], select {
padding: 4px 6px;
}
input[type="number"] {
width: 95px;
}
input.wide {
width: 360px;
}
.btn {
border: 1px solid var(--line);
background: #fff;
color: var(--text);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
}
.btn.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.error {
color: #b91c1c;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 8px 10px;
white-space: pre-wrap;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.subcard {
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
padding: 8px;
}
.subcard-title {
font-size: 13px;
font-weight: 700;
margin-bottom: 6px;
}
canvas {
border: 1px solid var(--line);
border-radius: 6px;
max-width: 100%;
height: auto;
background: #fff;
}
.stats {
color: var(--muted);
font-size: 12px;
margin-top: 6px;
white-space: pre-wrap;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th, td {
border: 1px solid var(--line);
padding: 4px 6px;
text-align: left;
}
@media (max-width: 980px) {
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="wrap">
<h1>Payload Detection Viewer</h1>
<p class="hint">
專為 POC手動選擇模型 (YOLOv5 / TinyYOLOv3 / FCOS) 後處理,將 payload 推論結果畫成框 + 類別 + 分數。
</p>
<div class="panel">
<div class="row">
<button id="parseBtn" class="btn primary">Parse Payload</button>
<button id="clearBtn" class="btn">Clear</button>
<label>原圖:</label>
<input id="imgInput" type="file" accept="image/*" />
<button id="clearImgBtn" class="btn">Clear Image</button>
</div>
<textarea id="payloadInput" placeholder="貼上完整 payload JSON..."></textarea>
</div>
<div id="errorBox" class="error" style="display:none"></div>
<div class="panel">
<div class="row">
<label>模型類型:</label>
<select id="modelType">
<option value="tinyyolo">TinyYOLOv3</option>
<option value="yolov5">YOLOv5</option>
<option value="fcos">FCOS</option>
</select>
<label>class 數:</label>
<input id="numClasses" type="number" min="1" value="80" />
<label>score 閾值:</label>
<input id="scoreTh" type="number" min="0" max="1" step="0.01" value="0.25" />
<label>NMS IoU:</label>
<input id="nmsTh" type="number" min="0" max="1" step="0.01" value="0.45" />
<label>max boxes:</label>
<input id="maxBoxes" type="number" min="1" value="200" />
</div>
<div class="row">
<label>score mode:</label>
<select id="scoreMode">
<option value="obj_cls">obj * cls</option>
<option value="obj">obj only</option>
<option value="cls">cls only</option>
</select>
<label><input id="objClsSigmoid" type="checkbox" checked /> obj/cls sigmoid</label>
<label><input id="yoloXySigmoid" type="checkbox" checked /> YOLO x/y sigmoid</label>
<button id="presetTinyBtn" class="btn">Preset TinyYOLO</button>
<button id="presetYolo5Btn" class="btn">Preset YOLOv5</button>
<button id="presetFcosBtn" class="btn">Preset FCOS</button>
</div>
<div class="row">
<label>模型輸入寬:</label>
<input id="inW" type="number" min="1" value="224" />
<label>模型輸入高:</label>
<input id="inH" type="number" min="1" value="224" />
<label>YOLO anchors:</label>
<input id="anchors" class="wide" type="text" value="10,14|23,27|37,58;81,82|135,169|344,319" />
</div>
<div class="row">
<label>FCOS class node idx(逗號):</label>
<input id="fcosClsIdx" class="wide" type="text" value="" placeholder="例如: 0,3,6" />
<label>FCOS reg node idx(逗號):</label>
<input id="fcosRegIdx" class="wide" type="text" value="" placeholder="例如: 1,4,7" />
<label>FCOS ctr node idx(逗號):</label>
<input id="fcosCtrIdx" class="wide" type="text" value="" placeholder="例如: 2,5,8" />
<button id="autoFcosIdxBtn" class="btn">Auto Fill FCOS idx</button>
</div>
<div class="row">
<label>FCOS strides:</label>
<input id="fcosStrides" class="wide" type="text" value="8,16,32,64,128" />
<button id="runBtn" class="btn primary">Decode + Draw</button>
</div>
</div>
<div class="grid">
<div class="subcard">
<div class="subcard-title">Detection Overlay</div>
<canvas id="overlayCanvas" width="1" height="1"></canvas>
<div id="overlayStats" class="stats"></div>
</div>
<div class="subcard">
<div class="subcard-title">Top Boxes</div>
<div style="max-height:420px; overflow:auto">
<table>
<thead>
<tr>
<th>#</th><th>cls</th><th>score</th><th>x1</th><th>y1</th><th>x2</th><th>y2</th>
</tr>
</thead>
<tbody id="boxTableBody"></tbody>
</table>
</div>
<div id="decodeStats" class="stats"></div>
<div id="debugStats" class="stats"></div>
</div>
</div>
</div>
<script>
const parseBtn = document.getElementById("parseBtn");
const clearBtn = document.getElementById("clearBtn");
const runBtn = document.getElementById("runBtn");
const clearImgBtn = document.getElementById("clearImgBtn");
const payloadInput = document.getElementById("payloadInput");
const imgInput = document.getElementById("imgInput");
const errorBox = document.getElementById("errorBox");
const overlayCanvas = document.getElementById("overlayCanvas");
const overlayStats = document.getElementById("overlayStats");
const decodeStats = document.getElementById("decodeStats");
const debugStats = document.getElementById("debugStats");
const boxTableBody = document.getElementById("boxTableBody");
const modelType = document.getElementById("modelType");
const numClasses = document.getElementById("numClasses");
const scoreTh = document.getElementById("scoreTh");
const nmsTh = document.getElementById("nmsTh");
const maxBoxes = document.getElementById("maxBoxes");
const inW = document.getElementById("inW");
const inH = document.getElementById("inH");
const anchorsInput = document.getElementById("anchors");
const fcosClsIdx = document.getElementById("fcosClsIdx");
const fcosRegIdx = document.getElementById("fcosRegIdx");
const fcosCtrIdx = document.getElementById("fcosCtrIdx");
const fcosStrides = document.getElementById("fcosStrides");
const autoFcosIdxBtn = document.getElementById("autoFcosIdxBtn");
const scoreMode = document.getElementById("scoreMode");
const objClsSigmoid = document.getElementById("objClsSigmoid");
const yoloXySigmoid = document.getElementById("yoloXySigmoid");
const presetTinyBtn = document.getElementById("presetTinyBtn");
const presetYolo5Btn = document.getElementById("presetYolo5Btn");
const presetFcosBtn = document.getElementById("presetFcosBtn");
let outputs = [];
let srcImg = null;
function showError(msg) {
errorBox.style.display = "block";
errorBox.textContent = msg;
}
function clearError() {
errorBox.style.display = "none";
errorBox.textContent = "";
}
function sigmoid(x) {
return 1 / (1 + Math.exp(-x));
}
function maybeSigmoid(v, on) {
return on ? sigmoid(v) : v;
}
function fmt(v) {
if (!Number.isFinite(v)) return "nan";
return Number(v).toFixed(4);
}
function decodeBase64Float32(base64String) {
const binary = atob(String(base64String || "").trim());
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const len = Math.floor(bytes.byteLength / 4);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const out = new Float32Array(len);
for (let i = 0; i < len; i++) out[i] = view.getFloat32(i * 4, true);
return out;
}
function arrProduct(a) {
let p = 1;
for (let i = 0; i < a.length; i++) p *= a[i];
return p;
}
function parsePayloadText() {
clearError();
const text = payloadInput.value.trim();
if (!text) throw new Error("請先貼 payload JSON");
let obj;
try {
obj = JSON.parse(text);
} catch (e) {
throw new Error("JSON parse failed: " + e.message);
}
const arr = obj?.data?.outputs || obj?.outputs;
if (!Array.isArray(arr) || arr.length === 0) {
throw new Error("找不到 outputs預期 payload.data.outputs");
}
const parsed = arr.map((o, i) => {
const shape = Array.isArray(o.shape) ? o.shape : [];
const data = decodeBase64Float32(o.data_base64);
const expected = shape.length ? arrProduct(shape) : data.length;
return {
idx: i,
node_idx: Number(o.node_idx ?? i),
shape,
data,
expected
};
});
outputs = parsed;
return parsed;
}
function parseAnchors(text) {
const levels = String(text || "").split(";").map(s => s.trim()).filter(Boolean);
return levels.map(level => level.split("|").map(pair => {
const [w, h] = pair.split(",").map(v => Number(v.trim()));
return [w, h];
}));
}
function pickYoloNodes(all) {
const c = Number(numClasses.value) || 80;
const picked = [];
for (const o of all) {
if (o.shape.length !== 4) continue;
if (o.shape[0] !== 1) continue;
const ch = o.shape[1];
if (ch % (5 + c) !== 0) continue;
picked.push(o);
}
picked.sort((a, b) => b.shape[2] - a.shape[2]);
return picked;
}
function decodeYoloCommon(all, mode) {
const c = Number(numClasses.value) || 80;
const confTh = Number(scoreTh.value) || 0.25;
const inpW = Number(inW.value) || 224;
const inpH = Number(inH.value) || 224;
const useSig = objClsSigmoid.checked;
const useXySig = yoloXySigmoid.checked;
const scoreModeValue = scoreMode.value;
const nodes = pickYoloNodes(all);
if (nodes.length === 0) throw new Error("??? YOLO ?? node????? [1,C,H,W] ? C ?? 5+classes ???");
const anchorLv = parseAnchors(anchorsInput.value);
const boxes = [];
const perLevel = [];
for (let lv = 0; lv < nodes.length; lv++) {
const o = nodes[lv];
const [, ch, gh, gw] = o.shape;
const attrs = 5 + c;
const na = Math.floor(ch / attrs);
const anchors = anchorLv[lv] || anchorLv[anchorLv.length - 1] || [];
let maxObj = -Infinity;
let maxCls = -Infinity;
let maxScore = -Infinity;
for (let a = 0; a < na; a++) {
const anchor = anchors[a] || [10, 10];
const aw = anchor[0];
const ah = anchor[1];
const baseA = a * attrs;
for (let y = 0; y < gh; y++) {
for (let x = 0; x < gw; x++) {
const idx = (ci) => {
const cidx = baseA + ci;
return cidx * gh * gw + y * gw + x;
};
const tx = o.data[idx(0)];
const ty = o.data[idx(1)];
const tw = o.data[idx(2)];
const th = o.data[idx(3)];
const to = o.data[idx(4)];
const obj = maybeSigmoid(to, useSig);
if (obj > maxObj) maxObj = obj;
let bestCls = -1;
let bestProb = -Infinity;
for (let k = 0; k < c; k++) {
const p = maybeSigmoid(o.data[idx(5 + k)], useSig);
if (p > bestProb) {
bestProb = p;
bestCls = k;
}
}
if (bestProb > maxCls) maxCls = bestProb;
let score;
if (scoreModeValue === "obj") score = obj;
else if (scoreModeValue === "cls") score = bestProb;
else score = obj * bestProb;
if (score > maxScore) maxScore = score;
if (score < confTh) continue;
let bx, by, bw, bh;
if (mode === "yolov5") {
const sx = inpW / gw;
const sy = inpH / gh;
const txv = useXySig ? sigmoid(tx) : tx;
const tyv = useXySig ? sigmoid(ty) : ty;
bx = (txv * 2 - 0.5 + x) * sx;
by = (tyv * 2 - 0.5 + y) * sy;
bw = Math.pow(sigmoid(tw) * 2, 2) * aw;
bh = Math.pow(sigmoid(th) * 2, 2) * ah;
} else {
const txv = useXySig ? sigmoid(tx) : tx;
const tyv = useXySig ? sigmoid(ty) : ty;
bx = (txv + x) / gw * inpW;
by = (tyv + y) / gh * inpH;
bw = aw * Math.exp(tw);
bh = ah * Math.exp(th);
}
boxes.push({
cls: bestCls,
score,
x1: bx - bw / 2,
y1: by - bh / 2,
x2: bx + bw / 2,
y2: by + bh / 2
});
}
}
}
perLevel.push({
lv,
shape: `[${o.shape.join(",")}]`,
maxObj,
maxCls,
maxScore
});
}
return {
boxes,
debug: {
type: mode,
scoreMode: scoreModeValue,
useSigmoid: useSig,
useXySigmoid: useXySig,
levels: perLevel
}
};
}
function parseIntList(text) {
return String(text || "")
.split(",")
.map(s => s.trim())
.filter(Boolean)
.map(v => Number(v));
}
function argmax(arr, start, len) {
let best = -1;
let bestVal = -Infinity;
for (let i = 0; i < len; i++) {
const v = arr[start + i];
if (v > bestVal) {
bestVal = v;
best = i;
}
}
return [best, bestVal];
}
function decodeFCOS(all) {
const c = Number(numClasses.value) || 80;
const confTh = Number(scoreTh.value) || 0.25;
const inpW = Number(inW.value) || 512;
const inpH = Number(inH.value) || 512;
const useSig = objClsSigmoid.checked;
const scoreModeValue = scoreMode.value;
const clsIdx = parseIntList(fcosClsIdx.value);
const regIdx = parseIntList(fcosRegIdx.value);
const ctrIdx = parseIntList(fcosCtrIdx.value);
const strides = parseIntList(fcosStrides.value);
if (clsIdx.length === 0 || regIdx.length === 0 || ctrIdx.length === 0) {
throw new Error("FCOS ?????? class/reg/centerness node idx??????");
}
if (!(clsIdx.length === regIdx.length && regIdx.length === ctrIdx.length)) {
throw new Error("FCOS node idx ???????? level ?? 1 ? class/reg/ctr?");
}
const boxes = [];
const perLevel = [];
for (let lv = 0; lv < clsIdx.length; lv++) {
const clsNode = all.find(o => o.node_idx === clsIdx[lv]);
const regNode = all.find(o => o.node_idx === regIdx[lv]);
const ctrNode = all.find(o => o.node_idx === ctrIdx[lv]);
const stride = strides[lv] || strides[strides.length - 1] || 8;
if (!clsNode || !regNode || !ctrNode) {
throw new Error(`FCOS level ${lv} node_idx ??????`);
}
if (clsNode.shape.length !== 4 || regNode.shape.length !== 4 || ctrNode.shape.length !== 4) {
throw new Error(`FCOS level ${lv} shape ??? [1,C,H,W]`);
}
const gh = clsNode.shape[2];
const gw = clsNode.shape[3];
const clsC = clsNode.shape[1];
const regC = regNode.shape[1];
const ctrC = ctrNode.shape[1];
if (regC < 4 || ctrC < 1) {
throw new Error(`FCOS level ${lv} reg/ctr ?????`);
}
let maxCtr = -Infinity;
let maxCls = -Infinity;
let maxScore = -Infinity;
for (let y = 0; y < gh; y++) {
for (let x = 0; x < gw; x++) {
const at = (node, ch) => node.data[ch * gh * gw + y * gw + x];
const ctrRaw = at(ctrNode, 0);
const center = maybeSigmoid(ctrRaw, useSig);
if (center > maxCtr) maxCtr = center;
let bestCls = -1;
let bestProb = -Infinity;
for (let k = 0; k < Math.min(c, clsC); k++) {
const p = maybeSigmoid(at(clsNode, k), useSig);
if (p > bestProb) {
bestProb = p;
bestCls = k;
}
}
if (bestProb > maxCls) maxCls = bestProb;
let score;
if (scoreModeValue === "obj") score = center;
else if (scoreModeValue === "cls") score = bestProb;
else score = Math.sqrt(Math.max(0, bestProb * center));
if (score > maxScore) maxScore = score;
if (score < confTh) continue;
const l = Math.max(0, at(regNode, 0));
const t = Math.max(0, at(regNode, 1));
const r = Math.max(0, at(regNode, 2));
const b = Math.max(0, at(regNode, 3));
const cx = (x + 0.5) * stride;
const cy = (y + 0.5) * stride;
const x1 = cx - l;
const y1 = cy - t;
const x2 = cx + r;
const y2 = cy + b;
if (x2 <= x1 || y2 <= y1) continue;
boxes.push({ cls: bestCls, score, x1, y1, x2, y2 });
}
}
perLevel.push({
lv,
clsShape: `[${clsNode.shape.join(",")}]`,
regShape: `[${regNode.shape.join(",")}]`,
ctrShape: `[${ctrNode.shape.join(",")}]`,
stride,
maxCtr,
maxCls,
maxScore
});
}
const clipped = boxes.map(b => ({
cls: b.cls,
score: b.score,
x1: Math.max(0, Math.min(inpW, b.x1)),
y1: Math.max(0, Math.min(inpH, b.y1)),
x2: Math.max(0, Math.min(inpW, b.x2)),
y2: Math.max(0, Math.min(inpH, b.y2))
}));
return {
boxes: clipped,
debug: {
type: "fcos",
scoreMode: scoreModeValue,
useSigmoid: useSig,
levels: perLevel
}
};
}
function iou(a, b) {
const xx1 = Math.max(a.x1, b.x1);
const yy1 = Math.max(a.y1, b.y1);
const xx2 = Math.min(a.x2, b.x2);
const yy2 = Math.min(a.y2, b.y2);
const w = Math.max(0, xx2 - xx1);
const h = Math.max(0, yy2 - yy1);
const inter = w * h;
if (inter <= 0) return 0;
const areaA = Math.max(0, a.x2 - a.x1) * Math.max(0, a.y2 - a.y1);
const areaB = Math.max(0, b.x2 - b.x1) * Math.max(0, b.y2 - b.y1);
return inter / Math.max(1e-9, areaA + areaB - inter);
}
function nms(boxes, iouTh, maxOut) {
const byCls = new Map();
for (const b of boxes) {
if (!byCls.has(b.cls)) byCls.set(b.cls, []);
byCls.get(b.cls).push(b);
}
const kept = [];
for (const [, arr] of byCls) {
arr.sort((a, b) => b.score - a.score);
const picked = [];
while (arr.length > 0) {
const cur = arr.shift();
picked.push(cur);
for (let i = arr.length - 1; i >= 0; i--) {
if (iou(cur, arr[i]) > iouTh) arr.splice(i, 1);
}
}
kept.push(...picked);
}
kept.sort((a, b) => b.score - a.score);
return kept.slice(0, maxOut);
}
function drawDetections(boxes) {
const ctx = overlayCanvas.getContext("2d");
const iw = Number(inW.value) || 224;
const ih = Number(inH.value) || 224;
const drawW = srcImg ? (srcImg.naturalWidth || srcImg.width) : iw;
const drawH = srcImg ? (srcImg.naturalHeight || srcImg.height) : ih;
overlayCanvas.width = drawW;
overlayCanvas.height = drawH;
ctx.clearRect(0, 0, drawW, drawH);
if (srcImg) {
ctx.drawImage(srcImg, 0, 0, drawW, drawH);
} else {
ctx.fillStyle = "#fff";
ctx.fillRect(0, 0, drawW, drawH);
}
const sx = drawW / iw;
const sy = drawH / ih;
ctx.lineWidth = 2;
ctx.font = "12px Arial";
for (const b of boxes) {
const x1 = b.x1 * sx;
const y1 = b.y1 * sy;
const x2 = b.x2 * sx;
const y2 = b.y2 * sy;
const w = Math.max(1, x2 - x1);
const h = Math.max(1, y2 - y1);
const hue = (b.cls * 47) % 360;
const color = `hsl(${hue} 90% 45%)`;
ctx.strokeStyle = color;
ctx.fillStyle = color;
ctx.strokeRect(x1, y1, w, h);
const tag = `${b.cls}:${b.score.toFixed(3)}`;
const tw = ctx.measureText(tag).width + 6;
const ty = Math.max(12, y1 - 3);
ctx.fillRect(x1, ty - 12, tw, 12);
ctx.fillStyle = "#fff";
ctx.fillText(tag, x1 + 3, ty - 2);
}
}
function fillTable(boxes) {
boxTableBody.innerHTML = "";
const top = boxes.slice(0, 200);
for (let i = 0; i < top.length; i++) {
const b = top[i];
const tr = document.createElement("tr");
tr.innerHTML = `<td>${i + 1}</td><td>${b.cls}</td><td>${b.score.toFixed(4)}</td><td>${b.x1.toFixed(1)}</td><td>${b.y1.toFixed(1)}</td><td>${b.x2.toFixed(1)}</td><td>${b.y2.toFixed(1)}</td>`;
boxTableBody.appendChild(tr);
}
}
function runDecode() {
clearError();
if (outputs.length === 0) {
showError("?? Parse Payload");
return;
}
try {
const type = modelType.value;
let result;
if (type === "yolov5") {
result = decodeYoloCommon(outputs, "yolov5");
} else if (type === "tinyyolo") {
result = decodeYoloCommon(outputs, "tinyyolo");
} else {
result = decodeFCOS(outputs);
}
const raw = result.boxes;
const iouTh = Number(nmsTh.value) || 0.45;
const maxOut = Number(maxBoxes.value) || 200;
const finalBoxes = nms(raw, iouTh, maxOut);
drawDetections(finalBoxes);
fillTable(finalBoxes);
overlayStats.textContent = `draw size: ${overlayCanvas.width}x${overlayCanvas.height}`;
decodeStats.textContent = `decoded raw: ${raw.length}
final after NMS: ${finalBoxes.length}`;
const lvLines = (result.debug?.levels || []).map((lv) => {
if (result.debug.type === "fcos") {
return `L${lv.lv} stride=${lv.stride} cls=${lv.clsShape} reg=${lv.regShape} ctr=${lv.ctrShape} maxCtr=${fmt(lv.maxCtr)} maxCls=${fmt(lv.maxCls)} maxScore=${fmt(lv.maxScore)}`;
}
return `L${lv.lv} shape=${lv.shape} maxObj=${fmt(lv.maxObj)} maxCls=${fmt(lv.maxCls)} maxScore=${fmt(lv.maxScore)}`;
});
debugStats.textContent = [
`decoder: ${result.debug?.type || type}`,
`scoreMode: ${result.debug?.scoreMode || scoreMode.value}`,
`obj/cls sigmoid: ${result.debug?.useSigmoid ?? objClsSigmoid.checked}`,
`yolo x/y sigmoid: ${result.debug?.useXySigmoid ?? yoloXySigmoid.checked}`,
...lvLines
].join("\n");
} catch (e) {
showError("Decode failed: " + e.message);
}
}
function applyTinyPreset() {
modelType.value = "tinyyolo";
numClasses.value = 80;
scoreTh.value = 0.25;
nmsTh.value = 0.45;
maxBoxes.value = 200;
inW.value = 224;
inH.value = 224;
anchorsInput.value = "10,14|23,27|37,58;81,82|135,169|344,319";
scoreMode.value = "obj_cls";
objClsSigmoid.checked = true;
yoloXySigmoid.checked = true;
}
function applyYolo5Preset() {
modelType.value = "yolov5";
numClasses.value = 80;
scoreTh.value = 0.25;
nmsTh.value = 0.45;
maxBoxes.value = 200;
inW.value = 640;
inH.value = 640;
anchorsInput.value = "10,14|23,27|37,58;81,82|135,169|344,319";
scoreMode.value = "obj_cls";
objClsSigmoid.checked = true;
yoloXySigmoid.checked = true;
}
function applyFcosPreset() {
modelType.value = "fcos";
numClasses.value = 80;
scoreTh.value = 0.25;
nmsTh.value = 0.45;
maxBoxes.value = 200;
inW.value = 512;
inH.value = 512;
fcosStrides.value = "8,16,32,64,128";
scoreMode.value = "obj_cls";
objClsSigmoid.checked = true;
if (outputs.length > 0) {
autoFillFcosIndices();
}
}
function autoFillFcosIndices() {
clearError();
if (!Array.isArray(outputs) || outputs.length === 0) {
showError("請先 Parse Payload才能自動填 FCOS idx");
return;
}
const c = Number(numClasses.value) || 80;
const valid = outputs.filter(o => o.shape.length === 4 && o.shape[0] === 1);
const clsNodes = valid.filter(o => o.shape[1] === c);
const regNodes = valid.filter(o => o.shape[1] === 4);
const ctrNodes = valid.filter(o => o.shape[1] === 1);
const byHW = (arr) => {
const m = new Map();
for (const o of arr) {
const key = `${o.shape[2]}x${o.shape[3]}`;
if (!m.has(key)) m.set(key, []);
m.get(key).push(o);
}
return m;
};
const clsMap = byHW(clsNodes);
const regMap = byHW(regNodes);
const ctrMap = byHW(ctrNodes);
const keys = [];
for (const key of clsMap.keys()) {
if (regMap.has(key) && ctrMap.has(key)) keys.push(key);
}
keys.sort((a, b) => {
const ah = Number(a.split("x")[0]);
const bh = Number(b.split("x")[0]);
return bh - ah;
});
const levels = keys.map((key) => {
const pickMinNode = (arr) => arr.slice().sort((x, y) => x.node_idx - y.node_idx)[0];
return {
key,
cls: pickMinNode(clsMap.get(key)),
reg: pickMinNode(regMap.get(key)),
ctr: pickMinNode(ctrMap.get(key))
};
});
if (levels.length === 0) {
showError("找不到可配對的 FCOS cls/reg/ctr node同 HxW");
return;
}
fcosClsIdx.value = levels.map(l => l.cls.node_idx).join(",");
fcosRegIdx.value = levels.map(l => l.reg.node_idx).join(",");
fcosCtrIdx.value = levels.map(l => l.ctr.node_idx).join(",");
const defaultStrides = [8, 16, 32, 64, 128];
fcosStrides.value = defaultStrides.slice(0, levels.length).join(",");
debugStats.textContent = [
"auto filled FCOS idx:",
`class: ${fcosClsIdx.value}`,
`reg: ${fcosRegIdx.value}`,
`ctr: ${fcosCtrIdx.value}`,
`levels(HxW): ${levels.map(l => l.key).join(",")}`
].join("\n");
}
parseBtn.addEventListener("click", () => {
clearError();
try {
const arr = parsePayloadText();
decodeStats.textContent = `parsed outputs: ${arr.length}`;
} catch (e) {
showError(e.message);
}
});
clearBtn.addEventListener("click", () => {
payloadInput.value = "";
outputs = [];
boxTableBody.innerHTML = "";
decodeStats.textContent = "";
debugStats.textContent = "";
overlayStats.textContent = "";
clearError();
});
imgInput.addEventListener("change", (evt) => {
const file = evt.target.files && evt.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
srcImg = img;
if (outputs.length > 0) runDecode();
};
img.src = String(reader.result || "");
};
reader.readAsDataURL(file);
});
clearImgBtn.addEventListener("click", () => {
srcImg = null;
imgInput.value = "";
if (outputs.length > 0) runDecode();
});
runBtn.addEventListener("click", runDecode);
presetTinyBtn.addEventListener("click", applyTinyPreset);
presetYolo5Btn.addEventListener("click", applyYolo5Preset);
presetFcosBtn.addEventListener("click", applyFcosPreset);
autoFcosIdxBtn.addEventListener("click", autoFillFcosIndices);
</script>
</body>
</html>

View File

@ -0,0 +1,624 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Payload Tensor Viewer</title>
<style>
:root {
--bg: #f5f6f8;
--card: #ffffff;
--text: #1f2937;
--muted: #6b7280;
--line: #d1d5db;
--accent: #2563eb;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: var(--bg);
color: var(--text);
}
.wrap {
max-width: 1200px;
margin: 20px auto;
padding: 0 12px 24px;
}
h1 {
margin: 0 0 8px;
}
.hint {
margin: 0 0 12px;
color: var(--muted);
font-size: 13px;
}
.panel {
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px;
margin-bottom: 12px;
}
.panel h2 {
margin: 0 0 8px;
font-size: 18px;
}
.panel .hint {
margin: 0 0 10px;
}
textarea {
width: 100%;
min-height: 220px;
box-sizing: border-box;
padding: 12px;
border: 1px solid var(--line);
border-radius: 8px;
font-family: Consolas, monospace;
background: #fff;
}
.controls {
margin: 10px 0 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
border: 1px solid var(--line);
background: var(--card);
color: var(--text);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
}
button.primary {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.error {
color: #b91c1c;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 12px;
white-space: pre-wrap;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
gap: 12px;
}
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
padding: 12px;
}
.title {
font-weight: 700;
margin-bottom: 4px;
}
.meta {
color: var(--muted);
font-size: 12px;
margin-bottom: 10px;
word-break: break-all;
}
.row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.row label {
font-size: 13px;
}
.row input[type="number"] {
width: 90px;
padding: 4px 6px;
}
.row input[type="range"] {
width: 160px;
}
.row select {
padding: 4px 6px;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 10px;
}
.subcard {
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px;
background: #fff;
}
.subcard-title {
font-weight: 700;
font-size: 13px;
margin-bottom: 6px;
}
canvas {
border: 1px solid var(--line);
border-radius: 6px;
background: #fff;
image-rendering: pixelated;
image-rendering: crisp-edges;
max-width: 100%;
height: auto;
}
.stats {
margin-top: 8px;
color: var(--muted);
font-size: 12px;
}
</style>
</head>
<body>
<div class="wrap">
<h1>Payload Tensor Viewer</h1>
<p class="hint">
Paste full JSON payload and click <b>Parse & Render</b>. Supports base64 float32 tensors in NCHW shape (e.g. [1,255,7,7]).
</p>
<textarea id="payloadInput" placeholder="Paste full payload JSON here..."></textarea>
<div class="controls">
<button id="parseBtn" class="primary">Parse & Render</button>
<button id="clearBtn">Clear</button>
</div>
<div id="errorBox" class="error" style="display:none"></div>
<div class="panel">
<h2>Overlay Viewer</h2>
<p class="hint">
Upload original image, pick output/channel, then overlay activation heatmap on top.
</p>
<div class="row">
<label>Image:</label>
<input id="imageInput" type="file" accept="image/*" />
<button id="clearImageBtn">Clear Image</button>
</div>
<div class="row">
<label>Output:</label>
<select id="overlayOutputSelect"></select>
<label>batch:</label>
<input id="overlayBatchInput" type="number" min="0" value="0" />
<label>channel:</label>
<input id="overlayChannelInput" type="number" min="0" value="0" />
<label>colormap:</label>
<select id="overlayMapSelect">
<option value="jet">jet</option>
<option value="gray">gray</option>
</select>
<label>alpha:</label>
<input id="overlayAlphaInput" type="range" min="0" max="1" step="0.01" value="0.45" />
<span id="overlayAlphaText">0.45</span>
<button id="overlayRenderBtn" class="primary">Render Overlay</button>
</div>
<div class="preview-grid">
<div class="subcard">
<div class="subcard-title">Overlay</div>
<canvas id="overlayCanvas" width="1" height="1"></canvas>
<div id="overlayStats" class="stats"></div>
</div>
<div class="subcard">
<div class="subcard-title">Heatmap Only</div>
<canvas id="heatmapCanvas" width="1" height="1"></canvas>
<div id="heatmapStats" class="stats"></div>
</div>
</div>
</div>
<div id="outputGrid" class="grid"></div>
</div>
<script>
const payloadInput = document.getElementById("payloadInput");
const parseBtn = document.getElementById("parseBtn");
const clearBtn = document.getElementById("clearBtn");
const errorBox = document.getElementById("errorBox");
const outputGrid = document.getElementById("outputGrid");
const imageInput = document.getElementById("imageInput");
const clearImageBtn = document.getElementById("clearImageBtn");
const overlayOutputSelect = document.getElementById("overlayOutputSelect");
const overlayBatchInput = document.getElementById("overlayBatchInput");
const overlayChannelInput = document.getElementById("overlayChannelInput");
const overlayMapSelect = document.getElementById("overlayMapSelect");
const overlayAlphaInput = document.getElementById("overlayAlphaInput");
const overlayAlphaText = document.getElementById("overlayAlphaText");
const overlayRenderBtn = document.getElementById("overlayRenderBtn");
const overlayCanvas = document.getElementById("overlayCanvas");
const heatmapCanvas = document.getElementById("heatmapCanvas");
const overlayStats = document.getElementById("overlayStats");
const heatmapStats = document.getElementById("heatmapStats");
let parsedOutputs = [];
let sourceImage = null;
function showError(message) {
errorBox.style.display = "block";
errorBox.textContent = message;
}
function clearError() {
errorBox.style.display = "none";
errorBox.textContent = "";
}
function decodeBase64Float32(base64String) {
const clean = String(base64String || "").trim();
const binary = atob(clean);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
const len = Math.floor(bytes.byteLength / 4);
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const out = new Float32Array(len);
for (let i = 0; i < len; i++) out[i] = view.getFloat32(i * 4, true);
return out;
}
function product(arr) {
return arr.reduce((a, b) => a * b, 1);
}
function minArray(values) {
let m = Number.POSITIVE_INFINITY;
for (let i = 0; i < values.length; i++) {
if (values[i] < m) m = values[i];
}
return m;
}
function maxArray(values) {
let m = Number.NEGATIVE_INFINITY;
for (let i = 0; i < values.length; i++) {
if (values[i] > m) m = values[i];
}
return m;
}
function slice2D(data, shape, batchIndex, channelIndex) {
if (!Array.isArray(shape) || shape.length < 2) {
throw new Error("Unsupported shape");
}
if (shape.length === 4) {
const n = shape[0], c = shape[1], h = shape[2], w = shape[3];
if (batchIndex < 0 || batchIndex >= n) throw new Error("Batch out of range");
if (channelIndex < 0 || channelIndex >= c) throw new Error("Channel out of range");
const out = new Float32Array(h * w);
let k = 0;
const base = ((batchIndex * c) + channelIndex) * h * w;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
out[k++] = data[base + y * w + x];
}
}
return { w, h, values: out };
}
if (shape.length === 3) {
const c = shape[0], h = shape[1], w = shape[2];
if (channelIndex < 0 || channelIndex >= c) throw new Error("Channel out of range");
const out = new Float32Array(h * w);
let k = 0;
const base = channelIndex * h * w;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
out[k++] = data[base + y * w + x];
}
}
return { w, h, values: out };
}
if (shape.length === 2) {
const h = shape[0], w = shape[1];
return { w, h, values: data.slice(0, h * w) };
}
throw new Error("Unsupported shape length: " + shape.length);
}
function drawGrayscale(canvas, values, w, h) {
const min = minArray(values);
const max = maxArray(values);
const span = max - min || 1;
const temp = document.createElement("canvas");
temp.width = w;
temp.height = h;
const tctx = temp.getContext("2d");
const img = tctx.createImageData(w, h);
for (let i = 0; i < values.length; i++) {
const v = Math.round(((values[i] - min) / span) * 255);
const p = i * 4;
img.data[p] = v;
img.data[p + 1] = v;
img.data[p + 2] = v;
img.data[p + 3] = 255;
}
tctx.putImageData(img, 0, 0);
const targetScale = Math.max(1, Math.floor(300 / Math.max(w, h)));
canvas.width = w * targetScale;
canvas.height = h * targetScale;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(temp, 0, 0, canvas.width, canvas.height);
return { min, max };
}
function jetColor(t) {
const x = Math.max(0, Math.min(1, t));
const r = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 3)));
const g = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 2)));
const b = Math.max(0, Math.min(1, 1.5 - Math.abs(4 * x - 1)));
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
function drawHeatmapToCanvas(canvas, values, w, h, mapMode) {
const min = minArray(values);
const max = maxArray(values);
const span = max - min || 1;
const temp = document.createElement("canvas");
temp.width = w;
temp.height = h;
const tctx = temp.getContext("2d");
const img = tctx.createImageData(w, h);
for (let i = 0; i < values.length; i++) {
const t = (values[i] - min) / span;
const p = i * 4;
let color;
if (mapMode === "gray") {
const g = Math.round(t * 255);
color = [g, g, g];
} else {
color = jetColor(t);
}
img.data[p] = color[0];
img.data[p + 1] = color[1];
img.data[p + 2] = color[2];
img.data[p + 3] = 255;
}
tctx.putImageData(img, 0, 0);
const targetScale = Math.max(1, Math.floor(300 / Math.max(w, h)));
canvas.width = w * targetScale;
canvas.height = h * targetScale;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(temp, 0, 0, canvas.width, canvas.height);
return { min, max, temp };
}
function renderOverlay() {
if (parsedOutputs.length === 0) {
overlayStats.textContent = "No payload parsed yet.";
heatmapStats.textContent = "";
return;
}
const outIndex = Number(overlayOutputSelect.value) || 0;
const out = parsedOutputs[outIndex];
if (!out) {
overlayStats.textContent = "Output not found.";
heatmapStats.textContent = "";
return;
}
try {
const b = Number(overlayBatchInput.value) || 0;
const ch = Number(overlayChannelInput.value) || 0;
const mapMode = overlayMapSelect.value || "jet";
const alpha = Number(overlayAlphaInput.value);
const slice = slice2D(out.data, out.shape, b, ch);
const h = drawHeatmapToCanvas(heatmapCanvas, Array.from(slice.values), slice.w, slice.h, mapMode);
heatmapStats.textContent = `Heatmap ${slice.w}x${slice.h}, min=${h.min.toFixed(6)}, max=${h.max.toFixed(6)}`;
const ctx = overlayCanvas.getContext("2d");
if (sourceImage) {
overlayCanvas.width = sourceImage.naturalWidth || sourceImage.width;
overlayCanvas.height = sourceImage.naturalHeight || sourceImage.height;
ctx.imageSmoothingEnabled = true;
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
ctx.globalAlpha = 1;
ctx.drawImage(sourceImage, 0, 0, overlayCanvas.width, overlayCanvas.height);
ctx.save();
ctx.globalAlpha = isNaN(alpha) ? 0.45 : alpha;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(h.temp, 0, 0, overlayCanvas.width, overlayCanvas.height);
ctx.restore();
overlayStats.textContent = `Overlay on image ${overlayCanvas.width}x${overlayCanvas.height}, alpha=${(isNaN(alpha) ? 0.45 : alpha).toFixed(2)}, output#${outIndex}, batch=${b}, channel=${ch}`;
} else {
overlayCanvas.width = heatmapCanvas.width;
overlayCanvas.height = heatmapCanvas.height;
ctx.imageSmoothingEnabled = false;
ctx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
ctx.drawImage(heatmapCanvas, 0, 0);
overlayStats.textContent = `No source image loaded, showing heatmap only. output#${outIndex}, batch=${b}, channel=${ch}`;
}
} catch (e) {
overlayStats.textContent = "Overlay render failed: " + e.message;
heatmapStats.textContent = "";
}
}
function refreshOverlayOutputOptions() {
overlayOutputSelect.innerHTML = "";
parsedOutputs.forEach((o, idx) => {
const opt = document.createElement("option");
opt.value = String(idx);
opt.textContent = `#${idx} node_idx=${o.item.node_idx} shape=${JSON.stringify(o.shape)}`;
overlayOutputSelect.appendChild(opt);
});
}
function buildOutputCard(item, idx) {
const shape = Array.isArray(item.shape) ? item.shape : [];
const data = decodeBase64Float32(item.data_base64);
const expected = shape.length ? product(shape) : data.length;
const card = document.createElement("div");
card.className = "card";
const title = document.createElement("div");
title.className = "title";
title.textContent = `Output #${idx} node_idx=${item.node_idx}`;
const meta = document.createElement("div");
meta.className = "meta";
meta.textContent = `dtype=${item.dtype || "unknown"}, shape=${JSON.stringify(shape)}, decoded=${data.length}, expected=${expected}`;
const row = document.createElement("div");
row.className = "row";
const batchLabel = document.createElement("label");
batchLabel.textContent = "batch:";
const batchInput = document.createElement("input");
batchInput.type = "number";
batchInput.value = 0;
batchInput.min = 0;
const channelLabel = document.createElement("label");
channelLabel.textContent = "channel:";
const channelInput = document.createElement("input");
channelInput.type = "number";
channelInput.value = 0;
channelInput.min = 0;
const renderBtn = document.createElement("button");
renderBtn.textContent = "Render Channel";
row.append(batchLabel, batchInput, channelLabel, channelInput, renderBtn);
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
const stats = document.createElement("div");
stats.className = "stats";
card.append(title, meta, row, canvas, stats);
const doRender = () => {
try {
const b = Number(batchInput.value) || 0;
const ch = Number(channelInput.value) || 0;
const slice = slice2D(data, shape, b, ch);
const st = drawGrayscale(canvas, Array.from(slice.values), slice.w, slice.h);
stats.textContent = `Rendered ${slice.w}x${slice.h}, min=${st.min.toFixed(6)}, max=${st.max.toFixed(6)}`;
} catch (e) {
stats.textContent = "Render failed: " + e.message;
}
};
renderBtn.addEventListener("click", doRender);
doRender();
return card;
}
function parsePayload() {
clearError();
outputGrid.innerHTML = "";
parsedOutputs = [];
overlayOutputSelect.innerHTML = "";
const text = payloadInput.value.trim();
if (!text) {
showError("Please paste payload JSON first.");
return;
}
let json;
try {
json = JSON.parse(text);
} catch (e) {
showError("JSON parse failed: " + e.message);
return;
}
const outputs = json?.data?.outputs || json?.outputs;
if (!Array.isArray(outputs) || outputs.length === 0) {
showError("No outputs found. Expected payload.data.outputs array.");
return;
}
try {
outputs.forEach((o, i) => {
parsedOutputs.push({
item: o,
data: decodeBase64Float32(o.data_base64),
shape: Array.isArray(o.shape) ? o.shape : []
});
outputGrid.appendChild(buildOutputCard(o, i));
});
refreshOverlayOutputOptions();
if (parsedOutputs.length > 0) {
overlayOutputSelect.value = "0";
renderOverlay();
}
} catch (e) {
showError("Render pipeline failed: " + e.message);
}
}
parseBtn.addEventListener("click", parsePayload);
clearBtn.addEventListener("click", () => {
payloadInput.value = "";
outputGrid.innerHTML = "";
parsedOutputs = [];
overlayOutputSelect.innerHTML = "";
clearError();
overlayStats.textContent = "";
heatmapStats.textContent = "";
});
imageInput.addEventListener("change", (evt) => {
const file = evt.target.files && evt.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
const img = new Image();
img.onload = () => {
sourceImage = img;
renderOverlay();
};
img.src = String(reader.result || "");
};
reader.readAsDataURL(file);
});
clearImageBtn.addEventListener("click", () => {
sourceImage = null;
imageInput.value = "";
renderOverlay();
});
overlayRenderBtn.addEventListener("click", renderOverlay);
overlayOutputSelect.addEventListener("change", renderOverlay);
overlayBatchInput.addEventListener("change", renderOverlay);
overlayChannelInput.addEventListener("change", renderOverlay);
overlayMapSelect.addEventListener("change", renderOverlay);
overlayAlphaInput.addEventListener("input", () => {
overlayAlphaText.textContent = overlayAlphaInput.value;
renderOverlay();
});
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -4,26 +4,26 @@
- YOLOv5 (model zoo)
- Model: `res/models/KL520/yolov5-noupsample_w640h640_kn-model-zoo/kl520_20005_yolov5-noupsample_w640h640.nef`
- Image: `res/images/one_bike_many_cars_800x800.bmp`
- Source: `examples_model_zoo/kl520_kn-model-zoo_generic_image_inference_post_yolov5/kl520_kn-model-zoo_generic_image_inference_post_yolov5.c`
- Source: `examples_model_zoo/kl520_kn-model-zoo_generic_inference_post_yolov5/kl520_kn-model-zoo_generic_inference_post_yolov5.c`
- FCOS (model zoo)
- Model: `res/models/KL520/fcos-drk53s_w512h512_kn-model-zoo/kl520_20004_fcos-drk53s_w512h512.nef`
- Image: `res/images/one_bike_many_cars_800x800.bmp`
- Source: `examples_model_zoo/kl520_kn-model-zoo_generic_image_inference_post_fcos/kl520_kn-model-zoo_generic_image_inference_post_fcos.c`
- Source: `examples_model_zoo/kl520_kn-model-zoo_generic_inference_post_fcos/kl520_kn-model-zoo_generic_inference_post_fcos.c`
- Tiny YOLO v3 (generic demo)
- Model: `res/models/KL520/tiny_yolo_v3/models_520.nef`
- Image: `res/images/bike_cars_street_224x224.bmp`
- Source: `examples/kl520_demo_generic_image_inference_post_yolo/kl520_demo_generic_image_inference_post_yolo.c`
- Source: `examples/kl520_demo_app_yolo_inference/kl520_demo_app_yolo_inference.c`
- Tiny YOLO v3 (multithread demo)
- Model: `res/models/KL520/tiny_yolo_v3/models_520.nef`
- Image: `res/images/bike_cars_street_224x224.bmp`
- Source: `examples/kl520_demo_generic_image_inference_multithread/kl520_demo_generic_image_inference_multithread.c`
- Source: `examples/kl520_demo_app_yolo_inference_multithread/kl520_demo_app_yolo_inference_multithread.c`
## KL720
- YOLOv5 (model zoo)
- Model: `res/models/KL720/yolov5-noupsample_w640h640_kn-model-zoo/kl720_20005_yolov5-noupsample_w640h640.nef`
- Image: `res/images/one_bike_many_cars_800x800.bmp`
- Source: `examples_model_zoo/kl720_kn-model-zoo_generic_image_inference_post_yolov5/kl720_kn-model-zoo_generic_image_inference_post_yolov5.c`
- Source: `examples_model_zoo/kl720_kn-model-zoo_generic_inference_post_yolov5/kl720_kn-model-zoo_generic_inference_post_yolov5.c`
- FCOS (model zoo)
- Model: `res/models/KL720/fcos-drk53s_w512h512_kn-model-zoo/kl720_20004_fcos-drk53s_w512h512.nef`
- Image: `res/images/one_bike_many_cars_800x800.bmp`
- Source: `examples_model_zoo/kl720_kn-model-zoo_generic_image_inference_post_fcos/kl720_kn-model-zoo_generic_image_inference_post_fcos.c`
- Source: `examples_model_zoo/kl720_kn-model-zoo_generic_inference_post_fcos/kl720_kn-model-zoo_generic_inference_post_fcos.c`

View File

@ -0,0 +1 @@
2.2.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
SDK-v2.5.7

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
2.2.0

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
SDK-v1.3.0

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
3.0.0

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.