金神农修改

hubei-jinshennong
LAPTOP-S9HJSOEB\昊天 4 months ago
parent d983a1939e
commit 1c5c74625e

@ -2,37 +2,37 @@
#其中,C1、C2、C3是int类型,ET、ED是布尔类型,C1直接是赋值初始位置,ED底部默认.0,ET顶部默认.1
#格式是{货架号}-{触发任务}[-输出]:{初始值}
001-C1:0
001-C2:4
001-C3:8
#001-C2:4
#001-C3:8
001-C1-out:48
001-C2-out:52
001-C3-out:56
001-ET-out:60
001-ED-out:60
#001-C2-out:52
#001-C3-out:56
#001-ET-out:60
#001-ED-out:60
002-C1:12
002-C2:16
002-C3:20
#002-C2:16
#002-C3:20
002-C1-out:62
002-C2-out:66
002-C3-out:70
002-ET-out:74
002-ED-out:74
#002-C2-out:66
#002-C3-out:70
#002-ET-out:74
#002-ED-out:74
003-C1:24
003-C2:28
003-C3:32
#003-C2:8
#003-C3:32
003-C1-out:76
003-C2-out:80
003-C3-out:84
003-ET-out:88
003-ED-out:88
#003-C2-out:80
#003-C3-out:84
#003-ET-out:88
#003-ED-out:88
004-C1:36
004-C2:40
004-C3:44
004-C1-out:90
004-C2-out:94
004-C3-out:98
004-ET-out:102
004-ED-out:102
#004-C2:40
#004-C3:44
#004-C1-out:90
#004-C2-out:94
#004-C3-out:98
#004-ET-out:102
#004-ED-out:102

@ -0,0 +1,594 @@
"""
箱子堆侧面竖线检测工具
用途:检测箱子堆侧面是否有箱子倾斜或突出
原理:SobelX梯度 → 逐行扫描 + 连续性跟踪 → 聚类成竖线 → 分析异常
"""
import cv2
import numpy as np
from pathlib import Path
from collections import Counter
# ============================================================
# 参数配置区 — 在这里调整所有控制参数
# ============================================================
class Config:
"""所有可调参数,集中管理"""
# ---- 输入/输出 ----
IMAGE_PATH = r"" # 输入图片路径(必填)
OUTPUT_PATH = None # 输出路径,None=自动生成 _inspected 后缀
SHOW_WINDOW = True # 是否弹窗显示结果
# ---- ROI 裁剪区域(在旋转之后应用)----
# 设为 None 表示不裁剪,使用整张图
ROI_X1 = None # 左边界(px),例如 100
ROI_Y1 = None # 上边界(px),例如 50
ROI_X2 = None # 右边界(px),例如 800
ROI_Y2 = None # 下边界(px),例如 900
# ---- 旋转(在裁剪之前应用)----
ROTATE_DEG = 0.0 # 旋转角度(度),正=逆时针。例如图片歪了3度就填 3.0
# ---- 边缘检测方向 ----
# "dark_to_light" = 从右到左,深→浅(SobelX 负值)
# "light_to_dark" = 从右到左,浅→深(SobelX 正值)
GRADIENT_DIR = "dark_to_light"
# ---- 扫描参数 ----
SCAN_STEP = 20 # 垂直扫描步长(px),每 N px 扫一行
BORDER_MARGIN = 30 # 排除左右边缘各 N px(避免边框噪点)
X_CLUSTER_GAP = 40 # X方向聚类间距(px),同一组内的最大间距
MIN_BRIGHTNESS = 25 # 行平均亮度阈值,低于此值的暗行直接跳过(0-255)
MAX_GROUPS = 1 # 最多保留几个竖线组(通常只关注主边框=1)
# ---- 梯度参数 ----
GAUSSIAN_KERNEL = 5 # 高斯模糊核大小(奇数)
MIN_GRADIENT_FIRST = 10.0 # 第一遍粗定位的最低梯度强度
MIN_GRADIENT_TRACK = 6.0 # 第二遍跟踪的最低梯度强度
TRACK_SMOOTH = 0.7 # 跟踪平滑系数(0-1),越大越稳定但响应慢
# ---- 异常检测阈值 ----
PROTRUDE_PX = 15.0 # 突出异常阈值(px):偏离中线的距离
PROTRUDE_MIN_CONSEC = 3 # 突出:最少连续 N 行都突出才算有效
PROTRUDE_PROXIMITY_PX = 10 # 突出:连续突出的各行 X 间距在此范围内才算同一处突出
TILT_DEG = 3.0 # 局部倾斜异常阈值(度):相邻两行之间的角度变化
TILT_MIN_CONSEC = 3 # 倾倒:最少连续 N 段都超过倾斜阈值才算有效
TILT_SIMILARITY_DEG = 2.0 # 倾倒:连续各段角度最大差异在此范围内才认为"相似"
def _build_gradient_map(gray: np.ndarray, direction: str) -> np.ndarray:
"""
SobelX 水平梯度图。
direction:
"dark_to_light" → 右暗左亮,保留 SobelX 负值
"light_to_dark" → 右亮左暗,保留 SobelX 正值
"""
k = Config.GAUSSIAN_KERNEL
blurred = cv2.GaussianBlur(gray, (k, k), 1.0)
sobel_x = cv2.Sobel(blurred, cv2.CV_32F, dx=1, dy=0, ksize=3)
if direction == "dark_to_light":
grad = np.maximum(-sobel_x, 0) # 右暗左亮
else: # light_to_dark
grad = np.maximum(sobel_x, 0) # 右亮左暗
return grad
def _row_brightness(gray_row: np.ndarray) -> float:
"""返回一行灰度的平均值(0-255)。"""
return float(np.mean(gray_row))
def extract_edges(gray: np.ndarray,
scan_step: int,
border_margin: int,
x_cluster_gap: int,
min_brightness: float,
gradient_dir: str,
max_groups: int = 1):
"""
从灰度图逐行扫描,使用连续性跟踪策略。
新增:跳过太暗的行(min_brightness)。
"""
h, w = gray.shape
grad = _build_gradient_map(gray, gradient_dir)
scan_rows = list(range(0, h, scan_step))
n_scan = len(scan_rows)
# ---- 第一遍:粗略定位主区域中心 X(跳过暗行) ----
rough_points = []
for y in scan_rows:
# 跳过太暗的行
if _row_brightness(gray[y, :]) < min_brightness:
continue
row_grad = grad[y, :]
lo = border_margin
hi = w - border_margin
if hi <= lo:
continue
# 找窗口内最大值位置
best_val = -1
best_x = -1
for i in range(lo, hi):
if row_grad[i] > best_val:
best_val = row_grad[i]
best_x = i
if best_x >= 0 and best_val >= Config.MIN_GRADIENT_FIRST:
rough_points.append(best_x)
if not rough_points:
return [], []
main_center = int(np.median(rough_points))
# ---- 第二遍:连续性跟踪(同样跳过暗行) ----
all_points = []
track_x = main_center
smooth = Config.TRACK_SMOOTH
for idx, y in enumerate(scan_rows):
# 跳过太暗的行
if _row_brightness(gray[y, :]) < min_brightness:
continue
row = grad[y, :]
# 策略A:在跟踪位置附近 ±x_cluster_gap 搜索
search_lo = max(border_margin, track_x - x_cluster_gap)
search_hi = min(w - border_margin, track_x + x_cluster_gap)
best_x = None
best_val = -1
for i in range(search_lo, search_hi):
if row[i] > best_val:
best_val = row[i]
best_x = i
# 窗口内太弱,尝试全局搜索
if best_x is not None and best_val < Config.MIN_GRADIENT_TRACK:
glo_lo = border_margin
glo_hi = w - border_margin
g_best_val = -1
g_best_x = None
for i in range(glo_lo, glo_hi):
if row[i] > g_best_val:
g_best_val = row[i]
g_best_x = i
if g_best_x is not None and g_best_val >= Config.MIN_GRADIENT_TRACK * 0.5:
best_x = g_best_x
best_val = g_best_val
else:
best_x = None
if best_x is not None:
all_points.append({
'x': int(best_x),
'y': y,
'row_idx': idx,
})
track_x = int(track_x * smooth + best_x * (1 - smooth))
if not all_points:
return [], []
# ---- 按 X 聚类 ----
all_points.sort(key=lambda p: p['x'])
clusters = []
cluster = [all_points[0]]
for i in range(1, len(all_points)):
if all_points[i]['x'] - cluster[-1]['x'] <= x_cluster_gap:
cluster.append(all_points[i])
else:
clusters.append(cluster)
cluster = [all_points[i]]
clusters.append(cluster)
# 过滤小簇
min_count = max(3, int(n_scan * 0.08))
clusters = [c for c in clusters if len(c) >= min_count]
# 只保留最显著的组
clusters.sort(key=len, reverse=True)
clusters = clusters[:max_groups]
# ---- 构建结果 ----
segments = []
groups_info = []
for gid, cl in enumerate(clusters):
cl_sorted = sorted(cl, key=lambda p: p['y'])
xs_arr = np.array([p['x'] for p in cl_sorted], dtype=np.float64)
ys_arr = np.array([p['y'] for p in cl_sorted], dtype=np.float64)
if len(xs_arr) >= 3:
coeffs = np.polyfit(ys_arr, xs_arr, 1)
angle_h = np.degrees(np.arctan(coeffs[0]))
else:
angle_h = 0.0
angle_v = round((90.0 - angle_h) % 180, 2)
median_x = int(np.median(xs_arr))
groups_info.append({
'group_id': gid,
'median_x': median_x,
'angle': angle_v,
'count': len(cl_sorted),
'y_min': int(ys_arr.min()) if len(ys_arr) else 0,
'y_max': int(ys_arr.max()) if len(ys_arr) else 0,
})
for p in cl_sorted:
segments.append({
'x': p['x'],
'y': p['y'],
'row_idx': p['row_idx'],
'group_id': gid,
'group_angle': angle_v,
})
segments.sort(key=lambda s: (s['group_id'], s['y']))
return segments, groups_info
# ============================================================
# 图像预处理:旋转 + ROI裁剪
# ============================================================
def preprocess_image(img: np.ndarray,
rotate_deg: float,
roi: tuple | None) -> tuple[np.ndarray, np.ndarray]:
"""
预处理:旋转 → ROI裁剪 → 返回 (处理后彩色图, 处理后灰度图)
roi = (x1, y1, x2, y2) 或 None
"""
result = img.copy()
# ---- 旋转 ----
if abs(rotate_deg) > 0.001:
h, w = result.shape[:2]
center = (w // 2, h // 2)
M = cv2.getRotationMatrix2D(center, rotate_deg, 1.0)
# 计算新图像尺寸以包含全部内容
cos_a = abs(M[0, 0])
sin_a = abs(M[0, 1])
new_w = int(h * sin_a + w * cos_a)
new_h = int(h * cos_a + w * sin_a)
M[0, 2] += (new_w - w) / 2
M[1, 2] += (new_h - h) / 2
result = cv2.warpAffine(result, M, (new_w, new_h),
flags=cv2.INTER_LINEAR,
borderValue=(0, 0, 0))
print(f" [预处理] 旋转 {rotate_deg}deg → {new_w}x{new_h}")
# ---- ROI 裁剪 ----
if roi is not None:
x1, y1, x2, y2 = roi
rh, rw = result.shape[:2]
x1 = max(0, min(x1, rw))
y1 = max(0, min(y1, rh))
x2 = max(0, min(x2, rw))
y2 = max(0, min(y2, rh))
if x2 > x1 and y2 > y1:
result = result[y1:y2, x1:x2]
print(f" [预处理] ROI裁剪 ({x1},{y1})-({x2},{y2}) → {result.shape[1]}x{result.shape[0]}")
gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
return result, gray
# ============================================================
# 异常分析
# ============================================================
def analyze_segments(segments: list,
groups_info: list,
protrude_px: float,
protrude_min_consec: int,
protrude_proximity_px: float,
tilt_deg: float,
tilt_min_consec: int,
tilt_similarity_deg: float) -> dict:
"""
异常分析(连续段判定):
【突出】同一组内连续 N 行都突出,且相邻突出行 X 间距 < proximity_px → 报异常
【倾倒】同一组内连续 N 段的局部倾斜都 > tilt_deg,且各段角度相近(< similarity_deg) → 报异常
"""
if not segments or not groups_info:
return {'alerts': [], 'stats': {'total_segments': 0, 'x_range': (0,0), 'groups': []}}
all_x = [s['x'] for s in segments]
x_low = float(np.percentile(all_x, 5))
x_high = float(np.percentile(all_x, 95))
stats = {'total_segments': len(segments), 'x_range': (x_low, x_high), 'groups': groups_info}
alerts = []
for ginfo in groups_info:
gid = ginfo['group_id']
median_x = float(ginfo['median_x'])
group_segs = [s for s in segments if s['group_id'] == gid]
if len(group_segs) < 2:
continue
# ============================================
# 1. 突出检测:连续 N 行突出 + 位置相近
# ============================================
# 标记每行是否突出
protruding_indices = [] # 在 group_segs 中的索引
for idx, seg in enumerate(group_segs):
off = abs(seg['x'] - median_x)
if off > protrude_px:
protruding_indices.append(idx)
# 找连续段
if protruding_indices:
runs = []
run_start = 0
for i in range(1, len(protruding_indices)):
gap = protruding_indices[i] - protruding_indices[i - 1]
# 索引连续(gap==1) 且 X 位置相近
prev_seg = group_segs[protruding_indices[i - 1]]
curr_seg = group_segs[protruding_indices[i]]
x_dist = abs(curr_seg['x'] - prev_seg['x'])
if gap <= 2 and x_dist <= protrude_proximity_px:
continue # 同一段
else:
run_len = i - run_start
if run_len >= protrude_min_consec:
runs.append(protruding_indices[run_start:i])
run_start = i
# 最后一段
run_len = len(protruding_indices) - run_start
if run_len >= protrude_min_consec:
runs.append(protruding_indices[run_start:])
for run in runs:
segs_in_run = [group_segs[i] for i in run]
offsets = [round(abs(s['x'] - median_x), 1) for s in segs_in_run]
avg_off = round(np.mean(offsets), 1)
sides = ['right' if s['x'] > median_x else 'left' for s in segs_in_run]
# 多数侧
from collections import Counter
main_side = Counter(sides).most_common(1)[0][0]
alerts.append({
'type': 'protrude',
'group_id': gid,
'side': main_side,
'y_start': segs_in_run[0]['y'],
'y_end': segs_in_run[-1]['y'],
'count': len(segs_in_run),
'avg_offset_px': avg_off,
'segments': segs_in_run,
})
# ============================================
# 2. 倾倒检测:连续 N 段局部倾斜都超标 + 角度相似
# ============================================
local_tilts = [] # [(seg_index, tilt_angle), ...]
for i in range(1, len(group_segs)):
p, c = group_segs[i - 1], group_segs[i]
dy = c['y'] - p['y']
if dy == 0:
continue
lt = abs(np.degrees(np.arctan((c['x'] - p['x']) / dy)))
local_tilts.append((i, round(lt, 2)))
# 找连续段:每段倾斜都超标 + 角度差异在 similarity 内
tilted_indices = [t for t in local_tilts if t[1] > tilt_deg]
if tilted_indices:
runs = []
run_start = 0
for i in range(1, len(tilted_indices)):
gap = tilted_indices[i][0] - tilted_indices[i - 1][0]
angle_diff = abs(tilted_indices[i][1] - tilted_indices[i - 1][1])
if gap <= 2 and angle_diff <= tilt_similarity_deg:
continue
else:
run_len = i - run_start
if run_len >= tilt_min_consec:
runs.append(tilted_indices[run_start:i])
run_start = i
run_len = len(tilted_indices) - run_start
if run_len >= tilt_min_consec:
runs.append(tilted_indices[run_start:])
for run in runs:
angles = [t[1] for t in run]
avg_angle = round(np.mean(angles), 2)
# 涉及的 segments
start_idx = run[0][0] - 1 # 局部倾斜的起点段索引
end_idx = run[-1][0]
involved = group_segs[start_idx:end_idx + 1]
alerts.append({
'type': 'tilt',
'group_id': gid,
'y_start': involved[0]['y'],
'y_end': involved[-1]['y'],
'count': len(run),
'avg_tilt_deg': avg_angle,
'max_tilt_deg': round(max(angles), 2),
'segments': involved,
})
return {'alerts': alerts, 'stats': stats}
# ============================================================
# 可视化 — 只画竖线,绿正常红异常
# ============================================================
def draw_results(img: np.ndarray, segments: list,
groups_info: list, alerts: list) -> np.ndarray:
"""
画竖线:绿色=正常,红色=异常。
异常按连续段标记(alert 里的 'segments' 字段给出具体行)。
"""
result = img.copy()
h, _ = result.shape[:2]
# 收集所有异常行(x, y)
alert_rows = set()
for a in alerts:
if 'segments' in a:
for s in a['segments']:
alert_rows.add((int(s['x']), int(s['y'])))
for ginfo in groups_info:
gid = ginfo['group_id']
for s in segments:
if s['group_id'] != gid:
continue
sx, sy = s['x'], s['y']
bad = (sx, sy) in alert_rows
cv2.line(result, (sx, max(0, sy - 10)), (sx, min(h, sy + 10)),
(0, 0, 255) if bad else (0, 220, 0), 3 if bad else 2)
return result
# ============================================================
# 主流程
# ============================================================
def inspect(image_path: str,
output_path: str = None,
show: bool = False,
rotate_deg: float = 0.0,
roi: tuple | None = None,
gradient_dir: str = "dark_to_light",
scan_step: int = 20,
border_margin: int = 30,
x_cluster_gap: int = 40,
min_brightness: float = 25.0,
max_groups: int = 1,
protrude_px: float = 15.0,
protrude_min_consec: int = 3,
protrude_proximity_px: float = 10.0,
tilt_deg: float = 3.0,
tilt_min_consec: int = 3,
tilt_similarity_deg: float = 2.0) -> dict:
img = cv2.imread(image_path)
if img is None:
raise FileNotFoundError(f"无法读取图片: {image_path}")
print(f"[输入] {image_path} 原始尺寸: {img.shape[1]}x{img.shape[0]}")
# ---- 预处理:旋转 + ROI裁剪 ----
processed_img, gray = preprocess_image(img, rotate_deg, roi)
# ---- 边缘提取 ----
segments, groups_info = extract_edges(
gray,
scan_step=scan_step,
border_margin=border_margin,
x_cluster_gap=x_cluster_gap,
min_brightness=min_brightness,
gradient_dir=gradient_dir,
max_groups=max_groups,
)
# ---- 异常分析 ----
result = analyze_segments(
segments, groups_info,
protrude_px=protrude_px,
protrude_min_consec=protrude_min_consec,
protrude_proximity_px=protrude_proximity_px,
tilt_deg=tilt_deg,
tilt_min_consec=tilt_min_consec,
tilt_similarity_deg=tilt_similarity_deg,
)
# ---- 可视化(在预处理后的图上画) ----
annotated = draw_results(processed_img, segments, groups_info, result['alerts'])
# ---- 保存 ----
if output_path is None:
p = Path(image_path)
output_path = str(p.parent / f"{p.stem}_inspected{p.suffix}")
cv2.imwrite(output_path, annotated)
print(f"[OK] 结果已保存: {output_path}")
if show:
cv2.imshow("Box Edge Inspector", annotated)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ---- 报告 ----
dir_label = "深→浅" if gradient_dir == "dark_to_light" else "浅→深"
print(f"\n{'='*50}")
print(f"检测完成: {image_path}")
print(f" 方向: {dir_label} | 旋转: {rotate_deg}deg")
if roi:
print(f" ROI: {roi}")
print(f" 扫描步长: {scan_step}px | 最小亮度: {min_brightness}")
print(f" 竖线段数: {result['stats']['total_segments']}")
print(f" 轮廓组数: {len(result['stats']['groups'])}")
print(f" X范围: [{result['stats']['x_range'][0]:.0f}, {result['stats']['x_range'][1]:.0f}]")
print(f" 突出阈值: {protrude_px}px 连续{protrude_min_consec}行 | 倾倒阈值: {tilt_deg}deg 连续{tilt_min_consec}段")
if result['alerts']:
print(f" ! 发现{len(result['alerts'])}处异常:")
for i, a in enumerate(result['alerts'], 1):
t = a['type']
if t == 'tilt':
print(f" {i}. [倾倒] Y={a['y_start']}~{a['y_end']} "
f"{a['count']}段 平均{a['avg_tilt_deg']}deg 最大{a['max_tilt_deg']}deg")
elif t == 'protrude':
print(f" {i}. [突出] Y={a['y_start']}~{a['y_end']} "
f"向{a['side']}{a['avg_offset_px']}px {a['count']}行")
else:
print(f" OK 无异常")
print('='*50)
return {
'ok': len(result['alerts'])==0,
'alerts': result['alerts'],
'stats': result['stats'],
'output_path': output_path,
}
# ============================================================
if __name__ == '__main__':
# ─── 所有参数在这里改 ──────────────────────────────
C = Config
inspect(
image_path=C.IMAGE_PATH,
output_path=C.OUTPUT_PATH,
show=C.SHOW_WINDOW,
# 几何变换
rotate_deg=C.ROTATE_DEG,
roi=(C.ROI_X1, C.ROI_Y1, C.ROI_X2, C.ROI_Y2) if C.ROI_X1 is not None else None,
# 检测方向
gradient_dir=C.GRADIENT_DIR, # "dark_to_light" 或 "light_to_dark"
# 扫描参数
scan_step=C.SCAN_STEP, # 每 N px 扫一行
border_margin=C.BORDER_MARGIN, # 排除边缘 N px
x_cluster_gap=C.X_CLUSTER_GAP, # 组内最大间距
min_brightness=C.MIN_BRIGHTNESS, # 暗行阈值,低于此跳过
max_groups=C.MAX_GROUPS, # 保留几个竖线组
# 异常阈值
protrude_px=C.PROTRUDE_PX,
protrude_min_consec=C.PROTRUDE_MIN_CONSEC,
protrude_proximity_px=C.PROTRUDE_PROXIMITY_PX,
tilt_deg=C.TILT_DEG,
tilt_min_consec=C.TILT_MIN_CONSEC,
tilt_similarity_deg=C.TILT_SIMILARITY_DEG,
)

@ -0,0 +1,426 @@
"""
密集堆叠箱子 YOLO 实例分割数据集生成器 v3
==============================================
整体流程:
1. 在临时画布上【正交拼接】所有箱子 → 无间隙、无黑色底色
2. 对整张拼图做【一次全局透视变换】→ 统一的俯视畸变
3. 贴到背景图【中间 60% 区域】
4. labels: 整个堆垛的 外轮廓多边形(所有箱子角点的凸包)
修复:
- 拼图画布用第一个箱子纹理铺底,零间隙 → 无黑色
- label 为整体堆垛多边形(如 3 箱合并为一个 6+ 点多边形)
"""
import cv2
import numpy as np
from pathlib import Path
import random
import os
from concurrent.futures import ThreadPoolExecutor, as_completed
# ============================================================
# 配置
# ============================================================
class Config:
# ---- 必填 ----
BOX_DIR = r"" # 箱子正面图文件夹
BG_DIR = r"" # 背景环境图文件夹
# ---- 输出 ----
OUTPUT_DIR = r"./box_dataset"
NUM_IMAGES = 500
TRAIN_RATIO = 0.8
IMG_WIDTH = 1280
IMG_HEIGHT = 960
# ---- 画面占比 ----
STACK_CENTER_RATIO = 0.60 # 堆垛占画面中间的比例(宽高各60%)
STACK_Y_OFFSET = 0.08 # 堆垛整体下移
# ---- 堆垛结构 ----
MIN_COLS = 2
MAX_COLS = 5
MIN_BASE_ROWS = 1
MAX_BASE_ROWS = 4
MAX_STACK_HEIGHT = 4
# ---- 箱子尺寸(正交画布上) ----
BOX_W_MIN = 80
BOX_W_MAX = 180
BOX_H_MIN = 100
BOX_H_MAX = 240
# ---- 箱子间距(设为0=零间隙,无黑色缝隙) ----
GAP_X = 0
GAP_Y = 0
# ---- 全局透视 (上边/下边) ----
PERSPECTIVE_TOP_RATIO = 1.05
PERSPECTIVE_VAR = 0.02
# ---- 箱子外观 ----
BRIGHTNESS_RANGE = (0.80, 1.25)
SATURATION_RANGE = (0.75, 1.20)
HUE_SHIFT_RANGE = (-12, 12)
# ---- 背景 ----
BG_BLUR_RANGE = (0, 3)
# ---- 多线程 ----
WORKERS = 4
# ============================================================
# 工具
# ============================================================
def load_images(directory: str) -> list:
exts = ('.jpg', '.jpeg', '.png', '.bmp', '.webp')
imgs = []
p = Path(directory)
if not p.exists():
return imgs
for f in sorted(p.iterdir()):
if f.suffix.lower() in exts:
img = cv2.imread(str(f))
if img is not None:
imgs.append(img)
return imgs
def adjust_color(img, brightness=1.0, saturation=1.0, hue_shift=0):
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype(np.float32)
hsv[:, :, 0] = (hsv[:, :, 0] + hue_shift) % 180
hsv[:, :, 1] = np.clip(hsv[:, :, 1] * saturation, 0, 255)
hsv[:, :, 2] = np.clip(hsv[:, :, 2] * brightness, 0, 255)
return cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2BGR)
# ============================================================
# 堆垛布局
# ============================================================
def build_stack_layout(cfg):
"""
正交坐标系中构建多层堆垛。
返回: boxes, comp_w, comp_h
"""
n_cols = random.randint(cfg.MIN_COLS, cfg.MAX_COLS)
n_base_rows = random.randint(cfg.MIN_BASE_ROWS, cfg.MAX_BASE_ROWS)
col_widths = [random.randint(cfg.BOX_W_MIN, cfg.BOX_W_MAX)
for _ in range(n_cols)]
row_heights = [random.randint(cfg.BOX_H_MIN, cfg.BOX_H_MAX)
for _ in range(n_base_rows)]
# 零间距计算坐标
col_x = [0]
for w in col_widths:
col_x.append(col_x[-1] + w)
row_y = [0]
for h in row_heights:
row_y.append(row_y[-1] + h)
boxes = []
for r in range(n_base_rows):
for c in range(n_cols):
boxes.append({
'row': r, 'col': c, 'layer': 0,
'x1': col_x[c],
'y1': row_y[r],
'x2': col_x[c + 1],
'y2': row_y[r + 1],
})
# 上层随机叠高
for layer in range(1, cfg.MAX_STACK_HEIGHT):
for r in range(n_base_rows):
for c in range(n_cols):
if random.random() < 0.5:
continue
below = [b for b in boxes
if b['row'] == r and b['col'] == c
and b['layer'] == layer - 1]
if not below:
continue
base = below[0]
shrink = random.uniform(0.82, 0.96)
bw, bh = base['x2'] - base['x1'], base['y2'] - base['y1']
nw = max(20, int(bw * shrink))
nh = max(20, int(bh * shrink))
nx1 = base['x1'] + (bw - nw) // 2
ny1 = base['y1'] - nh
boxes.append({
'row': r, 'col': c, 'layer': layer,
'x1': nx1, 'y1': ny1,
'x2': nx1 + nw, 'y2': ny1 + nh,
})
# 获取画布范围
xs = [b['x1'] for b in boxes] + [b['x2'] for b in boxes]
ys = [b['y1'] for b in boxes] + [b['y2'] for b in boxes]
comp_w = int(max(xs) - min(xs) + 2)
comp_h = int(max(ys) - min(ys) + 2)
dx = min(xs) - 1
dy = min(ys) - 1
for b in boxes:
b['x1'] -= dx
b['x2'] -= dx
b['y1'] -= dy
b['y2'] -= dy
return boxes, comp_w, comp_h
# ============================================================
# 拼图画布(无黑色底色)
# ============================================================
def build_composition(boxes_ortho, comp_w, comp_h, box_imgs, cfg):
"""
用第一个箱子纹理铺满整张画布,再在上面画各个箱子。
全程零间隙,无黑色出现。
"""
# 拿一个箱子纹理铺满画布(消除黑色底色)
base_tex = random.choice(box_imgs).copy()
base_tex = adjust_color(
base_tex,
brightness=random.uniform(*cfg.BRIGHTNESS_RANGE),
saturation=random.uniform(*cfg.SATURATION_RANGE),
hue_shift=int(random.uniform(*cfg.HUE_SHIFT_RANGE)),
)
comp = cv2.resize(base_tex, (comp_w, comp_h), interpolation=cv2.INTER_AREA)
# 按层排序(底层先画)
boxes_sorted = sorted(boxes_ortho, key=lambda b: (b['layer'], b['y1']))
for bi in boxes_sorted:
x1 = int(max(0, round(bi['x1'])))
y1 = int(max(0, round(bi['y1'])))
x2 = int(min(comp_w, round(bi['x2'])))
y2 = int(min(comp_h, round(bi['y2'])))
bw, bh = x2 - x1, y2 - y1
if bw < 3 or bh < 3:
continue
tex = random.choice(box_imgs).copy()
tex = adjust_color(
tex,
brightness=random.uniform(*cfg.BRIGHTNESS_RANGE),
saturation=random.uniform(*cfg.SATURATION_RANGE),
hue_shift=int(random.uniform(*cfg.HUE_SHIFT_RANGE)),
)
tex = cv2.resize(tex, (bw, bh), interpolation=cv2.INTER_AREA)
comp[y1:y2, x1:x2] = tex
# 箱间细微阴影(只画底层底部)
for bi in boxes_ortho:
if bi['layer'] != 0:
continue
x1 = int(max(0, round(bi['x1'])))
x2 = int(min(comp_w, round(bi['x2'])))
y2 = int(min(comp_h, round(bi['y2'])))
y1 = int(max(0, round(bi['y1'])))
if y2 - y1 < 20:
continue
shadow_h = max(1, (y2 - y1) // 30)
for si in range(shadow_h):
sy = y2 - shadow_h + si
a = 0.20 * (1 - si / shadow_h)
row = comp[sy, x1:x2].astype(np.float32) * (1 - a)
comp[sy, x1:x2] = np.clip(row, 0, 255).astype(np.uint8)
return comp
# ============================================================
# 单张生成
# ============================================================
def generate_one(idx, box_imgs, bg_imgs, cfg, out_img_dir, out_lbl_dir):
img_w, img_h = cfg.IMG_WIDTH, cfg.IMG_HEIGHT
# ---- 1. 正交布局 ----
boxes_ortho, comp_w, comp_h = build_stack_layout(cfg)
if not boxes_ortho:
return f"box_{idx:05d}.jpg", 0
# ---- 2. 拼图(零黑色) ----
comp = build_composition(boxes_ortho, comp_w, comp_h, box_imgs, cfg)
# ---- 3. 全局透视 warp ----
top_ratio = cfg.PERSPECTIVE_TOP_RATIO + random.uniform(
-cfg.PERSPECTIVE_VAR, cfg.PERSPECTIVE_VAR)
stack_w = int(img_w * cfg.STACK_CENTER_RATIO)
stack_h = int(img_h * cfg.STACK_CENTER_RATIO)
mid_x = img_w / 2.0
offset_y = int(img_h * cfg.STACK_Y_OFFSET)
top_y = int(img_h / 2 - stack_h / 2 + offset_y)
bot_y = top_y + stack_h
src_corners = np.float32([[0, 0], [comp_w, 0],
[comp_w, comp_h], [0, comp_h]])
dst_corners = np.float32([
[mid_x - (stack_w / 2) * top_ratio, top_y],
[mid_x + (stack_w / 2) * top_ratio, top_y],
[mid_x + stack_w / 2, bot_y],
[mid_x - stack_w / 2, bot_y],
])
M = cv2.getPerspectiveTransform(src_corners, dst_corners)
warped = cv2.warpPerspective(
comp, M, (img_w, img_h),
borderMode=cv2.BORDER_CONSTANT,
borderValue=(0, 0, 0),
)
mask = cv2.warpPerspective(
np.ones((comp_h, comp_w), dtype=np.uint8) * 255,
M, (img_w, img_h),
borderMode=cv2.BORDER_CONSTANT,
borderValue=0,
)
# ---- 4. 贴到背景 ----
bg = random.choice(bg_imgs).copy()
bg = cv2.resize(bg, (img_w, img_h))
bk = random.randint(*cfg.BG_BLUR_RANGE)
if bk >= 3:
if bk % 2 == 0:
bk += 1
bg = cv2.GaussianBlur(bg, (bk, bk), 0)
canvas = bg.copy()
valid = mask > 0
for ch in range(3):
canvas[:, :, ch] = np.where(valid, warped[:, :, ch],
canvas[:, :, ch])
# ---- 5. 标签:整体轮廓多边形 ----
# 收集所有箱子角点在最终图像中的坐标
all_corners_final = []
for bi in boxes_ortho:
corners_comp = np.float32([
[bi['x1'], bi['y1']],
[bi['x2'], bi['y1']],
[bi['x2'], bi['y2']],
[bi['x1'], bi['y2']],
])
ones = np.ones((4, 1), dtype=np.float32)
pts_h = np.hstack([corners_comp, ones])
pts_f = (M @ pts_h.T).T
pts_f[:, 0] /= pts_f[:, 2]
pts_f[:, 1] /= pts_f[:, 2]
all_corners_final.append(pts_f[:, :2])
all_pts = np.vstack(all_corners_final) # (N, 2)
# 凸包 → 整个堆垛的外轮廓
hull = cv2.convexHull(all_pts.astype(np.float32))
hull = hull.squeeze(1) # (N, 2)
# 简化(每隔1个点取一个,避免过多点)
if len(hull) > 12:
keep = np.arange(0, len(hull), 2)
hull = hull[keep]
norm_parts = []
for pt in hull:
norm_parts.append(f"{pt[0] / img_w:.6f}")
norm_parts.append(f"{pt[1] / img_h:.6f}")
label_line = "0 " + " ".join(norm_parts)
# ---- 保存 ----
fname = f"box_{idx:05d}.jpg"
cv2.imwrite(os.path.join(out_img_dir, fname), canvas,
[cv2.IMWRITE_JPEG_QUALITY, 95])
lbl_path = os.path.join(out_lbl_dir, f"box_{idx:05d}.txt")
with open(lbl_path, 'w') as f:
f.write(label_line + "\n")
return fname, 1 # 每张图1个标签(整体)
# ============================================================
# 主函数
# ============================================================
def main():
c = Config
if not c.BOX_DIR or not Path(c.BOX_DIR).exists():
raise FileNotFoundError("请设置 BOX_DIR")
if not c.BG_DIR or not Path(c.BG_DIR).exists():
raise FileNotFoundError("请设置 BG_DIR")
box_imgs = load_images(c.BOX_DIR)
bg_imgs = load_images(c.BG_DIR)
if len(box_imgs) < 2:
raise ValueError(f"箱子纹理太少({len(box_imgs)}),至少2张")
if len(bg_imgs) < 1:
raise ValueError("背景图为空")
print("=" * 55)
print(f"[纹理] 箱子: {len(box_imgs)}张 | 背景: {len(bg_imgs)}张")
print(f"[输出] {c.IMG_WIDTH}x{c.IMG_HEIGHT} | {c.NUM_IMAGES}张")
print(f"[布局] {c.MIN_COLS}-{c.MAX_COLS}列 x "
f"{c.MIN_BASE_ROWS}-{c.MAX_BASE_ROWS}行 | "
f"最高{c.MAX_STACK_HEIGHT}层")
print(f"[标签] 整体堆垛轮廓多边形(凸包)")
print("=" * 55)
out = Path(c.OUTPUT_DIR)
for split in ['train', 'val']:
(out / 'images' / split).mkdir(parents=True, exist_ok=True)
(out / 'labels' / split).mkdir(parents=True, exist_ok=True)
n_train = int(c.NUM_IMAGES * c.TRAIN_RATIO)
n_val = c.NUM_IMAGES - n_train
print(f"\n[生成] train={n_train} val={n_val}")
jobs = []
for i in range(c.NUM_IMAGES):
split = 'train' if i < n_train else 'val'
jobs.append((i, str(out / 'images' / split),
str(out / 'labels' / split)))
done = 0
with ThreadPoolExecutor(max_workers=c.WORKERS) as pool:
futures = {
pool.submit(generate_one, idx, box_imgs, bg_imgs, c, idir, ldir): idx
for idx, idir, ldir in jobs
}
for fut in as_completed(futures):
fname, _ = fut.result()
done += 1
if done % 50 == 0 or done == c.NUM_IMAGES:
print(f" [{done}/{c.NUM_IMAGES}] {fname}")
yaml_text = f"""# YOLO Instance Segmentation - Box Stack
path: {Path(c.OUTPUT_DIR).absolute()}
train: images/train
val: images/val
nc: 1
names:
0: box_stack
"""
with open(out / 'data.yaml', 'w') as f:
f.write(yaml_text)
print(f"\n{'=' * 55}")
print(f"完成! train={n_train} val={n_val}")
print(f"每图1个整体堆垛标签")
print(f"输出: {out.absolute()}")
print("=" * 55)
if __name__ == '__main__':
main()

@ -1,5 +1,6 @@
package com.zhehekeji.web;
import io.netty.util.ResourceLeakDetector;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@ -9,6 +10,11 @@ import org.springframework.cache.annotation.EnableCaching;
@MapperScan("com.zhehekeji.**.mapper.**")
public class Application {
public static void main(String[] args) {
// Netty 泄漏检测设置为 SIMPLE:仅采样检测,大幅降低检测开销,
// 避免泄漏检测本身影响其他业务的正常运行
// 级别:DISABLED(关) < SIMPLE(采样,推荐) < ADVANCED < PARANOID(全量,高开销)
ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.SIMPLE);
SpringApplication springApplication = new SpringApplication(Application.class);
springApplication.run(args);
}

@ -16,6 +16,8 @@ public interface OrderMapper extends BaseMapper<Order> {
@Select("select * from `order` where street_id = #{streetId} order by start_time desc limit 1")
Order getOneByStreetId(Integer streetId);
@Select("select * from `order` where street_id = #{streetId} and order_num= #{orderNumber} order by start_time desc limit 1")
Order getOneByStreetId(Integer streetId,String orderNumber);
List<OrderVO> list(@Param("req")OrderSearch orderSearch);
}

@ -14,6 +14,7 @@ import com.zhehekeji.web.pojo.stock.CheckStatus;
import com.zhehekeji.web.pojo.stock.RowColumnStatus;
import com.zhehekeji.web.service.client.Transmission;
import com.zhehekeji.web.service.cron.PLCConnectionExample;
import com.zhehekeji.web.service.cron.ZlmService;
import com.zhehekeji.web.service.ksec.KsecDataInfo;
import com.zhehekeji.web.service.ksec.KsecInfo;
import lombok.extern.slf4j.Slf4j;
@ -42,6 +43,12 @@ public class EmptyCheckService {
@Resource
private ConfigProperties configProperties;
@Resource
private ZlmService zlmService;
@Resource
CameraService cameraService;
@Resource
PLCConnectionExample plcConnectionExample;
/**
@ -51,24 +58,36 @@ public class EmptyCheckService {
*/
public Order visualJudgment(Transmission transmission,String code,int i){
String key = transmission.getSRMNumber() + "-";
Street street = getStreet(transmission);
log.info("transmission,{} key :{} value :{}",transmission.toString(),key + "-out");
if("TP".equals(code)){
key+="C3";
}else if("BP".equals(code)){
key+="C2";
}
Street street = getStreet(transmission);
log.info("transmission,{}",transmission.toString());
plcConnectionExample. writePlcDataTaskId(key + "-out");
// plcConnectionExample. writePlcDataTaskId(key + "-out");
if(street!= null){
Order order = orderMapper.getOneByStreetId(street.getId());
Order order = orderMapper.getOneByStreetId(street.getId(),transmission.getSRMNumber()+"_"+transmission.getTaskId());
if("BP".equals(code)){
if (order == null) {
log.error("未找到任务 "+transmission.getSRMNumber()+"_"+transmission.getTaskId());
return null;
}
order.setEndTime(LocalDateTime.now());
String path = zlmService.stopRecord(cameraService.getById(street.getCamera1Id()));
order.setVideoPath1(path);
}
transmission.setUrl(transmission.getUrl().replace("E:","").replace("D:",""));
transmission.setUrl("http://"+street.getPlcIp()+":9007/api/pic"+transmission.getUrl());
order.setEndTime(LocalDateTime.now());
order.setPicPaths(order.getPicPaths()!=null ? order.getPicPaths() + "," +transmission.getUrl(): transmission.getUrl());
if (transmission.getNgBoolean()){
order.setStatus(1);
plcConnectionService.writePlcDataStatusErr(street.getPlcId(),i);
}
// if (transmission.getNgBoolean()){
// order.setStatus(1);
// plcConnectionService.writePlcDataStatusErr(street.getPlcId(),i);
// }
orderMapper.updateById(order);
return order;
@ -78,16 +97,7 @@ public class EmptyCheckService {
}
@Resource
PLCConnectionExample plcConnectionService;
public Order visualJudgmentStatus(Transmission transmission,int i){
Street street = streetService.getStreetByPlcId(transmission.getSRMNumber());
if(street!= null){
Order order = orderMapper.getOneByStreetId(street.getId());
order.setStatus(1);
orderMapper.updateById(order);
plcConnectionService.writePlcDataStatusErr(street.getPlcId(),i);
}
return null;
}
public KsecInfo getKSECWriteByOrder(Order order,Street street){
KsecDataInfo ksecDataInfo = new KsecDataInfo();

@ -33,7 +33,15 @@ public class Decoder extends DelimiterBasedFrameDecoder {
private static String EMPTY_CLIENT = "EMPTY";
private static String END_STRING = "$";
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5,15,30, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(20000));
/**
* 隔离的线程池:使用 CallerRunsPolicy 反压策略,
* 队列满时在 Netty I/O 线程中直接执行,防止无限堆积 ByteBuf 撑爆内存
*/
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5, 15, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000), // 队列从 20000 降到 2000,限制最大堆积
new ThreadPoolExecutor.CallerRunsPolicy()
);
private EmptyCheckService emptyCheckService;
@ -56,10 +64,27 @@ public class Decoder extends DelimiterBasedFrameDecoder {
return null;
}
ClientRunnable clientRunnable = new ClientRunnable(in,ctx,plcService,emptyCheckService);
threadPoolExecutor.execute(clientRunnable);
try {
threadPoolExecutor.execute(clientRunnable);
} catch (Exception e) {
log.error("Decoder thread pool reject, release ByteBuf", e);
in.release(); // 线程池任务被拒绝时必须手动释放 ByteBuf
}
return null;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("Decoder channelInactive, release accumulated buffers");
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("Decoder exception, closing channel", cause);
ctx.close();
}
public static class ClientRunnable implements Runnable {
private ByteBuf in;
@ -79,35 +104,36 @@ public class Decoder extends DelimiterBasedFrameDecoder {
@Override
public void run() {
String body = in.toString(Charset.forName("UTF-8"));
tcpLogger.info("receive client:{}, data length:{}",body, body.length());
Transmission transmission = new Transmission(body);
if("BP".equals(transmission.getHeader())){
//底部拍照
Order order = emptyCheckService.visualJudgment(transmission,"BP",0);
//KsecInfo ksecInfo = emptyCheckService.getKSECWriteByOrder(order,street);
//KsecNettyClient.write(ksecInfo);
}else if("HB".equals(transmission.getHeader())){
//心跳
ClientChanel.connect(transmission.getSRMNumber(),ctx.channel());
}else if("TP".equals(transmission.getHeader())){
//顶部拍照
Order order = emptyCheckService.visualJudgment(transmission,"TP",1);
try {
String body = in.toString(Charset.forName("UTF-8"));
tcpLogger.info("receive client:{}, data length:{}",body, body.length());
Transmission transmission = new Transmission(body);
if("BP".equals(transmission.getHeader())){
//底部拍照
Order order = emptyCheckService.visualJudgment(transmission,"BP",0);
//KsecInfo ksecInfo = emptyCheckService.getKSECWriteByOrder(order,street);
//KsecNettyClient.write(ksecInfo);
}else if("HB".equals(transmission.getHeader())){
//心跳
ClientChanel.connect(transmission.getSRMNumber(),ctx.channel());
}else if("TP".equals(transmission.getHeader())){
//顶部拍照
Order order = emptyCheckService.visualJudgment(transmission,"TP",1);
// KsecInfo ksecInfo = emptyCheckService.getKSECWriteByOrder(order,street);
// KsecNettyClient.write(ksecInfo);
}else if("LC".equals(transmission.getHeader())){
//光源控制
}else if("LC".equals(transmission.getHeader())){
//光源控制
// ClientChanel.connect(transmission.getSRMNumber(),ctx.channel());
// ClientChanel.sendMessage(transmission.getSRMNumber(),transmission.getBody());
}else if("DC".equals(transmission.getHeader())){
//断连
ClientChanel.disConnect(transmission.getSRMNumber());
//ClientChanel.sendMessage(transmission.getSRMNumber(),transmission.getBody());
}
}else if("DC".equals(transmission.getHeader())){
//断连
ClientChanel.disConnect(transmission.getSRMNumber());
//ClientChanel.sendMessage(transmission.getSRMNumber(),transmission.getBody());
}
// else if("ED".equals(transmission.getHeader())){
// //底部报警
//
@ -119,6 +145,11 @@ public class Decoder extends DelimiterBasedFrameDecoder {
// emptyCheckService.visualJudgmentStatus(transmission,1);
// //ClientChanel.sendMessage(transmission.getSRMNumber(),transmission.getBody());
// }
} catch (Exception e) {
log.error("ClientRunnable process error", e);
} finally {
in.release(); // 确保无论如何都释放 ByteBuf,修复内存泄漏
}
}
}

@ -41,4 +41,10 @@ public class NettyConnectHandler extends ChannelInboundHandlerAdapter {
ClientChanel.disConnect(ID);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("NettyConnectHandler exception, closing channel", cause);
ctx.close();
}
}

@ -11,7 +11,7 @@ public class Transmission {
private String SRMNumber;
private Boolean success;
private String taskId;
private String url;
private String isNg;
private Boolean ngBoolean = false;
@ -33,7 +33,7 @@ public class Transmission {
this.SRMNumber= strings[1];
}
if(strings.length >= 4){
this.success= !strings[2].equals("0");
this.taskId= strings[2];
this.url= strings[3];
}
if(strings.length >= 5){

@ -258,6 +258,7 @@ public class PLCConnectionExample {
}
private static final int POLL_INTERVAL = 300; // 查询间隔,单位:毫秒
private static final int PLC_READ_TIMEOUT = 1; // PLC 读超时,单位:秒
// 读数据
@Scheduled(fixedDelay = POLL_INTERVAL)
void server(){
@ -319,10 +320,10 @@ public class PLCConnectionExample {
return 0;
}
public synchronized boolean writePlcDataTaskId(String key){
log.info("写入任务号:" + key);
for (String key1 : taskMap.keySet()){
log.info("taskMap key1:" + key1 + " value:" + taskMap.get(key1));
}
log.info("写入任务号:" + key+ " value:" + taskMap.get(key));
// for (String key1 : taskMap.keySet()){
// log.info("taskMap key1:" + key1 + " value:" + taskMap.get(key1));
// }
int value = taskMap.get(key);
if (value == 0) {
// 已经写入完成
@ -362,39 +363,48 @@ public class PLCConnectionExample {
return false;
}
/**
* 批量读取所有C1/C2/C3地址的任务号
* 批量读取所有C1/C2/C3地址的任务号(带超时保护)
* @return Map<key, value>,例如:{"001-C1": 100, "001-C2": 200, ...}
*/
public Map<String, Integer> readPlcDataTaskIds(){
S7Client client = getConnection();
Future<Map<String, Integer>> future = null;
try {
// 批量读取0-44字节区域
byte[] buffer = new byte[48];
int result = client.ReadArea(S7.S7AreaDB, dbNumber, 0, 48, buffer);
if (result != 0) {
updateConnection(client);
log.info("批量读取失败,错误码: " + result);
return new HashMap<>();
}
future = executorService.submit(() -> {
// 批量读取0-44字节区域
byte[] buffer = new byte[48];
int result = client.ReadArea(S7.S7AreaDB, dbNumber, 0, 48, buffer);
if (result != 0) {
log.info("批量读取失败,错误码: {}", result);
return null; // null 表示读取失败
}
// 解析所有C1/C2/C3地址的值
Map<String, Integer> resultMap = new HashMap<>();
for (String key : addressMap.keySet()) {
if (key.contains("out") || !key.contains("C")) {
continue;
// 解析所有C1/C2/C3地址的值
Map<String, Integer> resultMap = new HashMap<>();
for (String key : addressMap.keySet()) {
if (key.contains("out") || !key.contains("C")) {
continue;
}
int offset = addressMap.get(key);
int value = S7.GetDIntAt(buffer, offset);
resultMap.put(key, value);
}
int offset = addressMap.get(key);
int value = S7.GetDIntAt(buffer, offset);
resultMap.put(key, value);
}
return resultMap;
});
return resultMap;
Map<String, Integer> result = future.get(PLC_READ_TIMEOUT, TimeUnit.SECONDS);
returnConnection(client);
return result != null ? result : new HashMap<>();
} catch (TimeoutException e) {
log.error("批量读取 PLC 超时(>{}秒),强制断开并更新连接", PLC_READ_TIMEOUT);
future.cancel(true); // 中断阻塞的 ReadArea 调用
updateConnection(client);
return new HashMap<>();
} catch (Exception e) {
log.error("批量读取异常", e);
updateConnection(client);
return new HashMap<>();
} finally {
returnConnection(client);
}
}

@ -164,7 +164,7 @@ public class ZlmService {
if (result != null && Integer.parseInt(result.get("code").toString()) == 0) {
Map<String, Object> data = (Map<String, Object>) result.get("data");
String rootPath = data.get("rootPath").toString();
rootPath = rootPath.replace("E:\\data", "http://127.0.0.1:9007/api");
rootPath = rootPath.replace("E:\\data\\mp4", "");
List<String> paths = (List<String>) data.get("paths");
// 构建完整路径

@ -65,5 +65,10 @@ public class KescNettyHandler extends ChannelInboundHandlerAdapter {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("KescNettyHandler exception, closing channel", cause);
ctx.close();
}
}

@ -15,6 +15,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import io.netty.util.ReferenceCountUtil;
import java.nio.charset.Charset;
import java.time.LocalDate;
import java.util.HashMap;
@ -38,7 +39,15 @@ public class KsecDecoder extends DelimiterBasedFrameDecoder {
private static String lastLotnum;
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(7,21,30, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(20000));
/**
* 隔离的线程池:使用 CallerRunsPolicy 反压策略,
* 队列满时在 Netty I/O 线程中直接执行,防止无限堆积 ByteBuf 撑爆内存
*/
private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
7, 21, 30, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2000), // 队列从 20000 降到 2000,限制最大堆积
new ThreadPoolExecutor.CallerRunsPolicy()
);
private PlcService plcService;
@ -55,10 +64,31 @@ public class KsecDecoder extends DelimiterBasedFrameDecoder {
return null;
}
KescRunnable kescRunnable = new KescRunnable(in,ctx,plcService);
threadPoolExecutor.execute(kescRunnable);
try {
threadPoolExecutor.execute(kescRunnable);
} catch (Exception e) {
log.error("KsecDecoder thread pool reject, release ByteBuf", e);
in.release(); // 线程池任务被拒绝时必须手动释放 ByteBuf
}
return null;
}
/**
* 通道断开时清理父类 DelimiterBasedFrameDecoder 内部累积的 ByteBuf
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("KsecDecoder channelInactive, release accumulated buffers");
// 调用父类的 channelInactive,让 DelimiterBasedFrameDecoder 清理内部缓冲区
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("KsecDecoder exception, closing channel", cause);
ctx.close();
}
public static class KescRunnable implements Runnable{
private ByteBuf in;
@ -81,61 +111,61 @@ public class KsecDecoder extends DelimiterBasedFrameDecoder {
@Override
public void run() {
String body = in.toString(Charset.forName("UTF-8"));
tcpLogger.info(body);
if (body.startsWith("<")){
// 去掉首尾标识符
body = body.substring(1, body.length());
KsecInfo ksecInfo = JSONObject.parseObject(body, KsecInfo.class);
if (Cmd.A.name().equals(ksecInfo.getType())) {
in.release();
return ;
}
KsecDataInfo dataInfo = ksecInfo.getData();
plcService.upStreet(dataInfo);
String srmNumber = null;
String cmdName = null;
if(dataInfo != null){
LocalDate now = LocalDate.now();
if(dateMap.get(dataInfo.getTaskId()) == null){
dateMap.put(dataInfo.getTaskId(),now);
}else {
now = dateMap.get(dataInfo.getTaskId());
}
srmNumber = dataInfo.getSRMNumber();
cmdName = dataInfo.getCmdName();
}
if (Cmd.A.name().equals(ksecInfo.getType())) {
//心跳
log.debug("receieve heart ");
} else if (Cmd.B.name().equals(ksecInfo.getType())) {
//任务
if (Cmd.B1.name().equals(cmdName)) {
//B1 开始,开始录像
plcService.orderStart(dataInfo);
} else if (Cmd.B2.name().equals(cmdName)) {
//B2 C4 一起发的,需要停止等B2
plcService.orderStop(dataInfo);
dateMap.remove(dataInfo.getTaskId());
try {
String body = in.toString(Charset.forName("UTF-8"));
tcpLogger.info(body);
if (body.startsWith("<")){
// 去掉首尾标识符
body = body.substring(1, body.length());
KsecInfo ksecInfo = JSONObject.parseObject(body, KsecInfo.class);
if (Cmd.A.name().equals(ksecInfo.getType())) {
return ;
}
} else if (Cmd.C.name().equals(ksecInfo.getType())) {
//动作
String code = dataInfo.getCmdName();
log.info("action code,{},orderInfo:{}", code, dataInfo.toString());
if (Cmd.isBaseAction(code)) {
//执行动作,需要保存执行到第几步了
Integer times = GoodsActionTimes.put(dataInfo.getSRMNumber()+"_"+dataInfo.getTaskId());
plcService.action(dataInfo,times);
}else {
log.info("other C code :{}",code);
KsecDataInfo dataInfo = ksecInfo.getData();
plcService.upStreet(dataInfo);
String srmNumber = null;
String cmdName = null;
if(dataInfo != null){
LocalDate now = LocalDate.now();
if(dateMap.get(dataInfo.getTaskId()) == null){
dateMap.put(dataInfo.getTaskId(),now);
}else {
now = dateMap.get(dataInfo.getTaskId());
}
srmNumber = dataInfo.getSRMNumber();
cmdName = dataInfo.getCmdName();
}
} else if (Cmd.D.name().equals(ksecInfo.getType())) {
//柳州去掉告警
if (Cmd.A.name().equals(ksecInfo.getType())) {
//心跳
log.debug("receieve heart ");
} else if (Cmd.B.name().equals(ksecInfo.getType())) {
//任务
if (Cmd.B1.name().equals(cmdName)) {
//B1 开始,开始录像
plcService.orderStart(dataInfo);
} else if (Cmd.B2.name().equals(cmdName)) {
//B2 C4 一起发的,需要停止等B2
plcService.orderStop(dataInfo);
dateMap.remove(dataInfo.getTaskId());
}
} else if (Cmd.C.name().equals(ksecInfo.getType())) {
//动作
String code = dataInfo.getCmdName();
log.info("action code,{},orderInfo:{}", code, dataInfo.toString());
if (Cmd.isBaseAction(code)) {
//执行动作,需要保存执行到第几步了
Integer times = GoodsActionTimes.put(dataInfo.getSRMNumber()+"_"+dataInfo.getTaskId());
plcService.action(dataInfo,times);
}else {
log.info("other C code :{}",code);
}
} else if (Cmd.D.name().equals(ksecInfo.getType())) {
//柳州去掉告警
// String code = dataInfo.getCmdName();
// if(code.equals(Cmd.D1.name())){
// log.info("plcId:{},warn start",plcCmdInfo.getPlcId());
@ -153,11 +183,15 @@ public class KsecDecoder extends DelimiterBasedFrameDecoder {
// log.info("other D code :{}",code);
// }
}
//找到该货位的最后一张照片与现在的照片比照
//plcService.recordStock(plcCmdInfo, dataInfo.getCode(), 0, 0);
}
//找到该货位的最后一张照片与现在的照片比照
//plcService.recordStock(plcCmdInfo, dataInfo.getCode(), 0, 0);
} catch (Exception e) {
log.error("KescRunnable process error", e);
} finally {
in.release(); // 确保无论如何都释放 ByteBuf
}
in.release();
}
}
}

@ -66,5 +66,10 @@ public class HeartNettyClientHandler extends ChannelInboundHandlerAdapter {
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("streetId:{} HeartNettyClientHandler exception, closing channel", streetId, cause);
ctx.close();
}
}

@ -44,17 +44,17 @@ public class ProtocolDecoder extends FixedLengthFrameDecoder {
if (in == null) {
return null;
}
byte l = in.readByte();
byte p = in.readByte();
byte len1 = in.readByte();
byte len2 = in.readByte();
if (l != 76 && p != 80 && len1 != 4 && len2 != 3) {
//不是包头 丢
return null;
}
ByteBuf plc = in.readBytes(6);
String plcId = plc.toString(Charset.defaultCharset());
//CharSequence plc = in.readBytes(6, Charset.defaultCharset());
try {
byte l = in.readByte();
byte p = in.readByte();
byte len1 = in.readByte();
byte len2 = in.readByte();
if (l != 76 && p != 80 && len1 != 4 && len2 != 3) {
//不是包头 丢
return null;
}
ByteBuf plc = in.readBytes(6);
String plcId = plc.toString(Charset.defaultCharset());
//String plcId = plc.toString();
// OA=心跳 OB=工单 OC=任务 OD=告警
@ -157,9 +157,23 @@ public class ProtocolDecoder extends FixedLengthFrameDecoder {
plcService.warnStop(plcId);
}
}
} finally {
in.release();
return null;
}
return null;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("ProtocolDecoder channelInactive, clean FixedLengthFrameDecoder buffers");
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("ProtocolDecoder exception, closing channel", cause);
ctx.close();
}
/**

@ -44,4 +44,10 @@ public class SickClientHandler extends ChannelInboundHandlerAdapter {
//nettyClient.reconnect(sensorId);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("sensorId:{} exception, closing channel", sensorId, cause);
ctx.close();
}
}

@ -60,6 +60,19 @@ public class SickDecoder extends LineBasedFrameDecoder {
return null;
}
/**
* 通道断开时清理父类 LineBasedFrameDecoder 内部累积的 ByteBuf
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.debug("SickDecoder sensorId:{} channelInactive, clean buffers", sensorId);
super.channelInactive(ctx);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
log.error("SickDecoder sensorId:{} exception, closing channel", sensorId, cause);
ctx.close();
}
}

Loading…
Cancel
Save