feat(v1): chinese screenshot labels via Pillow with ASCII fallback
annotate_frame draws Chinese labels (e.g. '确认摔倒 P-0001') using a CJK font via Pillow when available (Windows uses Microsoft YaHei), and falls back to the ASCII cv2 label when Pillow or a CJK font is missing. 86 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -37,7 +37,7 @@
|
|||||||
| T-210 | 修复截图标注乱码(cv2 ASCII 文字) | T-202 | 截图标注文字改为纯 ASCII(`cv2.putText` 只支持 ASCII),去掉非 ASCII 分隔符导致的乱码;实时画面标签不变;ASCII 标签逻辑可测。 | DONE |
|
| T-210 | 修复截图标注乱码(cv2 ASCII 文字) | T-202 | 截图标注文字改为纯 ASCII(`cv2.putText` 只支持 ASCII),去掉非 ASCII 分隔符导致的乱码;实时画面标签不变;ASCII 标签逻辑可测。 | DONE |
|
||||||
| T-211 | 检测灵敏度参数进设置页 | T-209 | 设置页可编辑 `require_rapid_drop`/`require_lower_body`/`horizontal_angle_threshold_degrees`/`confirm_window_seconds`(下拉/数字/勾选),保存写回未跟踪 `config.local.json`、下次启动生效;写回逻辑可测。 | DONE |
|
| T-211 | 检测灵敏度参数进设置页 | T-209 | 设置页可编辑 `require_rapid_drop`/`require_lower_body`/`horizontal_angle_threshold_degrees`/`confirm_window_seconds`(下拉/数字/勾选),保存写回未跟踪 `config.local.json`、下次启动生效;写回逻辑可测。 | DONE |
|
||||||
| T-212 | 实时流只处理最新帧降延迟 | T-203 | STREAM 后台读帧只保留最新帧、丢弃积压,实时延迟不随推理速度增长;REPLAY 逐帧不丢;最新帧保留逻辑可测。 | DONE |
|
| T-212 | 实时流只处理最新帧降延迟 | T-203 | STREAM 后台读帧只保留最新帧、丢弃积压,实时延迟不随推理速度增长;REPLAY 逐帧不丢;最新帧保留逻辑可测。 | DONE |
|
||||||
| T-213 | 截图中文标注(Pillow) | T-210 | CONFIRMED 截图可用中文标注(如“确认摔倒 P-0001”);缺 Pillow/字体时回退 ASCII 不崩溃;标签与回退逻辑可测。 | DOING |
|
| T-213 | 截图中文标注(Pillow) | T-210 | CONFIRMED 截图可用中文标注(如“确认摔倒 P-0001”);缺 Pillow/字体时回退 ASCII 不崩溃;标签与回退逻辑可测。 | DONE |
|
||||||
|
|
||||||
## Phase 3 · Go V2 迁移门槛与实现
|
## Phase 3 · Go V2 迁移门槛与实现
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
- V1 代码:已建立安全配置、显式 Replay/Stream 视频源、Pose、轻量跟踪、质量/几何证据、倒地领域规则、按 ID 四态事件机及 `v1/pipeline.py` 事件管线;运行事件带非敏感 `config_version`,缺帧/低质量/断流会中断证据确认。新增 `v1/view_model.py`(Qt-free 监控视图状态与设置草稿三份隔离)、`v1/gui.py` 薄 PyQt5 双 Tab 外壳与 `v1/app.py` 装配(`FrameWorker` 只发出已判定的 `FrameAnalysis`,窗口只渲染)。新增 `v1/alerts.py`:按 `event_id` 去重,对首次 CONFIRMED 保存带标注截图、追加 JSONL 事件行,并经可注入 `AlertSink`(Windows 侧 `QtAlertSink` 提供声音与一次性弹窗)触发一次声音/弹窗。新增 `v1/camera.py`(RTSP URL 构建 + 有界连接测试抓帧)与 `config.py` 的结构化来源(host/端口/通道/账号/密码,凭证百分号编码)、`write_local_camera_source`;设置页新增摄像头连接分组、测试连接与预览,以及传输协议/连接超时/低延迟三项抓流调优(由 `config` 存值、`app` 开流前写入 `OPENCV_FFMPEG_CAPTURE_OPTIONS`)。真实海康流已用 `VideoSource(STREAM)` 连通(1920×1080)。
|
- V1 代码:已建立安全配置、显式 Replay/Stream 视频源、Pose、轻量跟踪、质量/几何证据、倒地领域规则、按 ID 四态事件机及 `v1/pipeline.py` 事件管线;运行事件带非敏感 `config_version`,缺帧/低质量/断流会中断证据确认。新增 `v1/view_model.py`(Qt-free 监控视图状态与设置草稿三份隔离)、`v1/gui.py` 薄 PyQt5 双 Tab 外壳与 `v1/app.py` 装配(`FrameWorker` 只发出已判定的 `FrameAnalysis`,窗口只渲染)。新增 `v1/alerts.py`:按 `event_id` 去重,对首次 CONFIRMED 保存带标注截图、追加 JSONL 事件行,并经可注入 `AlertSink`(Windows 侧 `QtAlertSink` 提供声音与一次性弹窗)触发一次声音/弹窗。新增 `v1/camera.py`(RTSP URL 构建 + 有界连接测试抓帧)与 `config.py` 的结构化来源(host/端口/通道/账号/密码,凭证百分号编码)、`write_local_camera_source`;设置页新增摄像头连接分组、测试连接与预览,以及传输协议/连接超时/低延迟三项抓流调优(由 `config` 存值、`app` 开流前写入 `OPENCV_FFMPEG_CAPTURE_OPTIONS`)。真实海康流已用 `VideoSource(STREAM)` 连通(1920×1080)。
|
||||||
- V2 代码:`v2/` 目录存在但尚无实现。
|
- V2 代码:`v2/` 目录存在但尚无实现。
|
||||||
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md、docs/ui/silver-pose-v1-prototype.html 与 docs/ui/silver-pose-v2-prototype.html 已建立。v2 HTML 是符合正式浅色 Windows 规范的当前视觉参考:浅灰蓝底、白色卡片,红色只表示确认摔倒、其弹窗和事件证据;文件名中的 v2 只表示原型设计修订,不能理解为 Go V2 实现已开始。v1 HTML 保留为历史深色对照。两者均使用顶部双 Tab、设置草稿与状态交互,且画面、事件和时间都是模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
- 非代码设计工件:docs/ui/silver-pose-ui-ux-spec.md、docs/ui/2026-07-20-html-prototype-plan.md、docs/ui/silver-pose-v1-prototype.html 与 docs/ui/silver-pose-v2-prototype.html 已建立。v2 HTML 是符合正式浅色 Windows 规范的当前视觉参考:浅灰蓝底、白色卡片,红色只表示确认摔倒、其弹窗和事件证据;文件名中的 v2 只表示原型设计修订,不能理解为 Go V2 实现已开始。v1 HTML 保留为历史深色对照。两者均使用顶部双 Tab、设置草稿与状态交互,且画面、事件和时间都是模拟数据,不连接真实摄像头、模型或网络,也不改变 Phase 1 任务顺序。
|
||||||
- 测试:`python -m compileall -q v1 demo` 已通过(含 `gui.py`、`app.py`、`alerts.py`、`camera.py` 语法);`python -m pytest v1/tests -v` 当前有 82 项配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断、灵敏度放宽、截图 ASCII 标注、断流重连、事件汇总、灵敏度写回和最新帧丢弃测试并已通过(含来源模式显式声明、模型置信度经适配器生效、事件号跨轮唯一、结构化凭证不入 config_version 且公开示例无凭证的守卫)。`demo/1.mp4` 的首两帧回放时间戳已验证为 0.000000 与 0.033333 秒;T-106 的真实模型/录像 smoke 在首帧得到 2 名已分析人员、第二帧得到 0 名人员且未创建事件;T-201 的视图冒烟以真实录像解码 + 真实管线 + 确定性假 Pose 适配器驱动 `build_monitor_view`,得到稳定 ID、box、14 段骨架、17/17 关键点与 NORMAL/success,断流帧 0 人且不显示摔倒标签;T-202 的报警冒烟用 `demo/1.mp4` 首帧(848×480)落盘一张可被 `cv2.imread` 读回的标注截图(480×848×3,约 330 KB)与一行 JSONL(相对截图路径、含 config_version/source_id、无 rtsp),重复派发返回 0。这些只验证管线、视图与报警工件可运行,不表示摔倒识别准确率。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
- 测试:`python -m compileall -q v1 demo` 已通过(含 `gui.py`、`app.py`、`alerts.py`、`camera.py` 语法);`python -m pytest v1/tests -v` 当前有 86 项配置、视频源、Pose、跟踪、证据、领域规则、状态机、管线、视图模型、报警工件、摄像头/URL 构建、抓流参数拼串、每帧诊断、灵敏度放宽、截图 ASCII 标注、断流重连、事件汇总、灵敏度写回、最新帧丢弃和截图中文标注测试并已通过(含来源模式显式声明、模型置信度经适配器生效、事件号跨轮唯一、结构化凭证不入 config_version 且公开示例无凭证的守卫)。`demo/1.mp4` 的首两帧回放时间戳已验证为 0.000000 与 0.033333 秒;T-106 的真实模型/录像 smoke 在首帧得到 2 名已分析人员、第二帧得到 0 名人员且未创建事件;T-201 的视图冒烟以真实录像解码 + 真实管线 + 确定性假 Pose 适配器驱动 `build_monitor_view`,得到稳定 ID、box、14 段骨架、17/17 关键点与 NORMAL/success,断流帧 0 人且不显示摔倒标签;T-202 的报警冒烟用 `demo/1.mp4` 首帧(848×480)落盘一张可被 `cv2.imread` 读回的标注截图(480×848×3,约 330 KB)与一行 JSONL(相对截图路径、含 config_version/source_id、无 rtsp),重复派发返回 0。这些只验证管线、视图与报警工件可运行,不表示摔倒识别准确率。`init.ps1` 会检查运行时依赖、编译旧基线并运行 V1 测试,但不会安装软件包。
|
||||||
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
- 模型:`demo/best.pt` 可加载为 YOLO Pose,类别 `person`,`kpt_shape=[17, 3]`;与 `D:\PythonP\fall_detection\best.pt` 哈希一致。
|
||||||
- 当前标准启动:`./init.ps1`。
|
- 当前标准启动:`./init.ps1`。
|
||||||
- 当前标准验证:`python -m compileall -q demo`。
|
- 当前标准验证:`python -m compileall -q demo`。
|
||||||
|
|||||||
@@ -406,3 +406,12 @@
|
|||||||
- 阻塞:真实中文观感需 Windows(用 C:/Windows/Fonts/msyh.ttc);标签、字体查找与回退逻辑本环境可测。
|
- 阻塞:真实中文观感需 Windows(用 C:/Windows/Fonts/msyh.ttc);标签、字体查找与回退逻辑本环境可测。
|
||||||
- 决策:auto 字体候选只放 CJK 字体(msyh/simhei/文泉驿/Noto CJK),找不到就回退 ASCII 而非渲染豆腐块;Windows 自动用雅黑,无需配置。
|
- 决策:auto 字体候选只放 CJK 字体(msyh/simhei/文泉驿/Noto CJK),找不到就回退 ASCII 而非渲染豆腐块;Windows 自动用雅黑,无需配置。
|
||||||
- 下一步:加 `annotation_label`/`_find_font`/PIL 绘制与回退,`annotate_frame` 接入,补测试。
|
- 下一步:加 `annotation_label`/`_find_font`/PIL 绘制与回退,`annotate_frame` 接入,补测试。
|
||||||
|
|
||||||
|
## 【2026-07-22】T-213 截图中文标注(Pillow)(完成)
|
||||||
|
|
||||||
|
- 状态:DONE
|
||||||
|
- 变更:`alerts.py` 新增 `annotation_label`("确认摔倒 P-0001" 等中文)、`_find_font`(只在 CJK 字体候选中查找:Windows msyh/simhei、Linux 文泉驿/Noto CJK)、`_draw_labels`/`_draw_labels_pil`。`annotate_frame(image, view, font_path=None)`:cv2 画框/骨架/红边后,若 PIL 可用且找到 CJK 字体则 PIL 绘中文标签,否则回退 cv2 ASCII(保留 T-210)。`requirements.txt` 加 `Pillow==9.5.0`。
|
||||||
|
- 验证:新增 test_alerts 四项(中文标签、字体查找有无、无字体回退不崩溃、有 TTF 时 PIL 路径运行);`python3 -m pytest v1/tests -q` 为 86 passed;`python3 -m compileall -q v1` 退出码 0。本环境无 CJK 字体故 auto 走 ASCII 回退;用 DejaVu 显式路径验证 PIL 绘制路径不崩溃(中文字形需 Windows 雅黑)。
|
||||||
|
- 阻塞:真实中文观感需 Windows(自动用 C:/Windows/Fonts/msyh.ttc)。
|
||||||
|
- 决策:auto 字体候选只放 CJK 字体,找不到就回退 ASCII 而非渲染豆腐块;无需配置,Windows 自动中文、其它环境安全回退。
|
||||||
|
- 下一步:本批"别的"三项(T-211/212/213)完成;用户 Windows 冒烟中文标注、低延迟与灵敏度设置页。
|
||||||
|
|||||||
+82
-7
@@ -15,6 +15,12 @@ from typing import Callable, Dict, Optional, Sequence, Tuple
|
|||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
_PIL_AVAILABLE = True
|
||||||
|
except ImportError: # pragma: no cover - optional dependency
|
||||||
|
_PIL_AVAILABLE = False
|
||||||
|
|
||||||
from v1.fall_state import FallEvent, FallState
|
from v1.fall_state import FallEvent, FallState
|
||||||
from v1.view_model import MonitorViewState, StatusColor
|
from v1.view_model import MonitorViewState, StatusColor
|
||||||
|
|
||||||
@@ -27,6 +33,37 @@ _COLOR_BGR: Dict[StatusColor, Tuple[int, int, int]] = {
|
|||||||
StatusColor.OFFLINE: (139, 116, 100),
|
StatusColor.OFFLINE: (139, 116, 100),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_STATE_LABEL_ZH: Dict[FallState, str] = {
|
||||||
|
FallState.NORMAL: "正常",
|
||||||
|
FallState.SUSPECT: "疑似",
|
||||||
|
FallState.CONFIRMED: "确认摔倒",
|
||||||
|
FallState.RECOVERING: "恢复中",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only CJK-capable fonts, so auto-discovery never renders tofu for Chinese;
|
||||||
|
# absent one, annotation falls back to ASCII. Windows finds Microsoft YaHei.
|
||||||
|
_FONT_CANDIDATES: Tuple[str, ...] = (
|
||||||
|
"C:/Windows/Fonts/msyh.ttc",
|
||||||
|
"C:/Windows/Fonts/msyhbd.ttc",
|
||||||
|
"C:/Windows/Fonts/simhei.ttf",
|
||||||
|
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||||
|
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def annotation_label(track_id: str, state: FallState) -> str:
|
||||||
|
"""Chinese screenshot label, e.g. '确认摔倒 P-0001'."""
|
||||||
|
|
||||||
|
return "{0} {1}".format(_STATE_LABEL_ZH.get(state, getattr(state, "value", state)), track_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_font(font_path: Optional[str] = None) -> Optional[str]:
|
||||||
|
candidates = [font_path] if font_path else list(_FONT_CANDIDATES)
|
||||||
|
for path in candidates:
|
||||||
|
if path and Path(path).is_file():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AlertRecord:
|
class AlertRecord:
|
||||||
@@ -58,8 +95,14 @@ def screenshot_label(track_id: str, state_value: str) -> str:
|
|||||||
return text.encode("ascii", "ignore").decode("ascii")
|
return text.encode("ascii", "ignore").decode("ascii")
|
||||||
|
|
||||||
|
|
||||||
def annotate_frame(image: np.ndarray, view: MonitorViewState) -> np.ndarray:
|
def annotate_frame(
|
||||||
"""Return a copy of ``image`` with boxes, skeleton, labels and a state border."""
|
image: np.ndarray, view: MonitorViewState, font_path: Optional[str] = None
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""Return a copy of ``image`` with boxes, skeleton, a state border and labels.
|
||||||
|
|
||||||
|
Labels are Chinese via Pillow when a CJK font is available, otherwise an
|
||||||
|
ASCII fallback via ``cv2.putText`` (which cannot render non-ASCII glyphs).
|
||||||
|
"""
|
||||||
|
|
||||||
canvas = image.copy()
|
canvas = image.copy()
|
||||||
for overlay in view.people:
|
for overlay in view.people:
|
||||||
@@ -77,6 +120,24 @@ def annotate_frame(image: np.ndarray, view: MonitorViewState) -> np.ndarray:
|
|||||||
for point in overlay.keypoints:
|
for point in overlay.keypoints:
|
||||||
if point is not None:
|
if point is not None:
|
||||||
cv2.circle(canvas, (int(round(point.x)), int(round(point.y))), 3, color, -1)
|
cv2.circle(canvas, (int(round(point.x)), int(round(point.y))), 3, color, -1)
|
||||||
|
if view.highest_state is FallState.CONFIRMED:
|
||||||
|
height, width = canvas.shape[:2]
|
||||||
|
cv2.rectangle(
|
||||||
|
canvas, (0, 0), (width - 1, height - 1), _COLOR_BGR[StatusColor.CRITICAL], 6
|
||||||
|
)
|
||||||
|
return _draw_labels(canvas, view, font_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_labels(
|
||||||
|
canvas: np.ndarray, view: MonitorViewState, font_path: Optional[str]
|
||||||
|
) -> np.ndarray:
|
||||||
|
resolved = _find_font(font_path)
|
||||||
|
if _PIL_AVAILABLE and resolved:
|
||||||
|
return _draw_labels_pil(canvas, view, resolved)
|
||||||
|
for overlay in view.people:
|
||||||
|
color = _COLOR_BGR[overlay.color]
|
||||||
|
left = int(round(overlay.box_xyxy[0]))
|
||||||
|
top = int(round(overlay.box_xyxy[1]))
|
||||||
cv2.putText(
|
cv2.putText(
|
||||||
canvas,
|
canvas,
|
||||||
screenshot_label(overlay.track_id, overlay.state.value),
|
screenshot_label(overlay.track_id, overlay.state.value),
|
||||||
@@ -87,14 +148,28 @@ def annotate_frame(image: np.ndarray, view: MonitorViewState) -> np.ndarray:
|
|||||||
1,
|
1,
|
||||||
cv2.LINE_AA,
|
cv2.LINE_AA,
|
||||||
)
|
)
|
||||||
if view.highest_state is FallState.CONFIRMED:
|
|
||||||
height, width = canvas.shape[:2]
|
|
||||||
cv2.rectangle(
|
|
||||||
canvas, (0, 0), (width - 1, height - 1), _COLOR_BGR[StatusColor.CRITICAL], 6
|
|
||||||
)
|
|
||||||
return canvas
|
return canvas
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_labels_pil(
|
||||||
|
canvas: np.ndarray, view: MonitorViewState, font_path: str
|
||||||
|
) -> np.ndarray:
|
||||||
|
rgb = Image.fromarray(np.ascontiguousarray(canvas[:, :, ::-1]))
|
||||||
|
draw = ImageDraw.Draw(rgb)
|
||||||
|
font = ImageFont.truetype(font_path, 18)
|
||||||
|
for overlay in view.people:
|
||||||
|
blue, green, red = _COLOR_BGR[overlay.color]
|
||||||
|
left = int(round(overlay.box_xyxy[0]))
|
||||||
|
top = int(round(overlay.box_xyxy[1]))
|
||||||
|
draw.text(
|
||||||
|
(left, max(0, top - 24)),
|
||||||
|
annotation_label(overlay.track_id, overlay.state),
|
||||||
|
fill=(red, green, blue),
|
||||||
|
font=font,
|
||||||
|
)
|
||||||
|
return np.ascontiguousarray(np.array(rgb)[:, :, ::-1])
|
||||||
|
|
||||||
|
|
||||||
class EventArtifactWriter:
|
class EventArtifactWriter:
|
||||||
"""Persist one annotated screenshot and one JSONL line per confirmed event."""
|
"""Persist one annotated screenshot and one JSONL line per confirmed event."""
|
||||||
|
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ numpy==1.24.4
|
|||||||
opencv-python==4.6.0.66
|
opencv-python==4.6.0.66
|
||||||
ultralytics==8.3.205
|
ultralytics==8.3.205
|
||||||
PyQt5==5.15.9
|
PyQt5==5.15.9
|
||||||
|
Pillow==9.5.0
|
||||||
pytest==8.3.5
|
pytest==8.3.5
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ from datetime import datetime, timezone
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
from v1.alerts import (
|
from v1.alerts import (
|
||||||
AlertDispatcher,
|
AlertDispatcher,
|
||||||
AlertSink,
|
AlertSink,
|
||||||
EventArtifactWriter,
|
EventArtifactWriter,
|
||||||
annotate_frame,
|
annotate_frame,
|
||||||
|
annotation_label,
|
||||||
screenshot_label,
|
screenshot_label,
|
||||||
|
_find_font,
|
||||||
)
|
)
|
||||||
from v1.fall_state import FallEvent, FallState
|
from v1.fall_state import FallEvent, FallState
|
||||||
from v1.view_model import MonitorViewState, PersonOverlay, Point, StatusColor
|
from v1.view_model import MonitorViewState, PersonOverlay, Point, StatusColor
|
||||||
@@ -141,3 +145,34 @@ def test_annotate_draws_red_border_only_for_confirmed():
|
|||||||
corner = confirmed[0, 0].tolist()
|
corner = confirmed[0, 0].tolist()
|
||||||
assert corner == [40, 40, 198] # critical red in BGR
|
assert corner == [40, 40, 198] # critical red in BGR
|
||||||
assert confirmed.shape == image.shape
|
assert confirmed.shape == image.shape
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotation_label_is_chinese():
|
||||||
|
assert annotation_label("P-0001", FallState.CONFIRMED) == "确认摔倒 P-0001"
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_font_returns_none_when_missing_and_path_when_present(tmp_path):
|
||||||
|
assert _find_font("/no/such/font.ttf") is None
|
||||||
|
font = tmp_path / "x.ttf"
|
||||||
|
font.write_bytes(b"stub")
|
||||||
|
assert _find_font(str(font)) == str(font)
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotate_falls_back_without_a_font():
|
||||||
|
image = np.full((60, 80, 3), 30, dtype=np.uint8)
|
||||||
|
|
||||||
|
out = annotate_frame(image, _confirmed_view(), font_path="/no/such/font.ttf")
|
||||||
|
|
||||||
|
assert out.shape == image.shape # ASCII fallback ran, no crash
|
||||||
|
|
||||||
|
|
||||||
|
def test_annotate_draws_labels_with_a_ttf_font():
|
||||||
|
font = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||||
|
if not os.path.isfile(font):
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.skip("no ttf available to exercise the PIL path")
|
||||||
|
|
||||||
|
out = annotate_frame(np.full((60, 80, 3), 30, dtype=np.uint8), _confirmed_view(), font_path=font)
|
||||||
|
|
||||||
|
assert out.shape == (60, 80, 3)
|
||||||
|
|||||||
Reference in New Issue
Block a user