feat(inspection): 增加巡检点位语料配置功能
- 在 inspection_waypoint 表中新增 chinese_corpus 和 english_corpus 字段存储中英文语料 - 创建 InspectionWaypointCorpusConfigVO 用于巡检点位语料查询节点参数 - 实现 InspectionWaypointCorpusMapper 提供按检测项ID查询语料的数据库操作 - 开发 InspectionWaypointCorpusOperateService 处理语料类型识别和语料获取逻辑 - 添加语料类型支持包括中文/英文、zh-CN/en-US、zh/en或0/1等多种格式 - 实现语料验证机制确保巡检点位配置了相应语言语料 - 编写单元测试验证中英文语料获取和类型解析功能 - 为 VisionLocateOperateService 增加完整的测试用例覆盖安全方向校准和平面定位功能
This commit is contained in:
parent
9cd3e4a5d6
commit
d4fb83ee53
@ -311,6 +311,7 @@ public class TeFlowController extends BaseController {
|
||||
@ApiOperation(value = "单个节点执行",
|
||||
notes = "设备通用指令action=DEVICE_EXECUTE_JSON_COMMAND,payload需要robotId、deviceId、requestJson;"
|
||||
+ "巡检仪表节点action=INSPECTION_METER_RECOGNIZE,payload使用InspectionMeterRecognizeConfigVO;"
|
||||
+ "点位语料节点action=INSPECTION_WAYPOINT_CORPUS,payload需要detectItemId、type;"
|
||||
+ "人工判断节点action=INSPECTION_MANUAL_REVIEW_CREATE,payload使用InspectionManualReviewConfigVO。"
|
||||
+ "该接口用于试调节点,不会生成正式巡检任务结果")
|
||||
public AjaxResult actionExecute(
|
||||
|
||||
@ -60,7 +60,7 @@ spring:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: test
|
||||
active: aima
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
|
||||
@ -45,6 +45,14 @@ public class InspectionWaypoint extends BaseEntity
|
||||
@ApiModelProperty("检测项配置id")
|
||||
private String detectItemId;
|
||||
|
||||
@Excel(name = "中文语料")
|
||||
@ApiModelProperty("该点位可播放的中文语料")
|
||||
private String chineseCorpus;
|
||||
|
||||
@Excel(name = "英文语料")
|
||||
@ApiModelProperty("该点位可播放的英文语料")
|
||||
private String englishCorpus;
|
||||
|
||||
@Excel(name = "地图id")
|
||||
@ApiModelProperty("地图id")
|
||||
private String mapId;
|
||||
|
||||
@ -48,6 +48,16 @@ public class InspectionWaypointVo extends BaseEntity
|
||||
@ApiModelProperty("检测项配置id")
|
||||
private String detectItemId;
|
||||
|
||||
/** 中文语料 */
|
||||
@Excel(name = "中文语料")
|
||||
@ApiModelProperty("该点位可播放的中文语料")
|
||||
private String chineseCorpus;
|
||||
|
||||
/** 英文语料 */
|
||||
@Excel(name = "英文语料")
|
||||
@ApiModelProperty("该点位可播放的英文语料")
|
||||
private String englishCorpus;
|
||||
|
||||
/** 配置状态 */
|
||||
@Excel(name = "配置状态")
|
||||
@ApiModelProperty("配置状态")
|
||||
|
||||
@ -102,6 +102,8 @@ public class InspectionWaypointServiceImpl extends ServiceImpl<InspectionWaypoin
|
||||
.enabled(source.getEnabled())
|
||||
.sortOrder(source.getSortOrder())
|
||||
.detectItemId(copiedDetectItemId)
|
||||
.chineseCorpus(source.getChineseCorpus())
|
||||
.englishCorpus(source.getEnglishCorpus())
|
||||
.mapId(source.getMapId())
|
||||
.build();
|
||||
copy.setRemark(source.getRemark());
|
||||
|
||||
@ -15,6 +15,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="enabled" column="enabled" />
|
||||
<result property="sortOrder" column="sort_order" />
|
||||
<result property="detectItemId" column="detect_item_id" />
|
||||
<result property="chineseCorpus" column="chinese_corpus" />
|
||||
<result property="englishCorpus" column="english_corpus" />
|
||||
<result property="mapId" column="map_id" />
|
||||
</resultMap>
|
||||
|
||||
@ -29,6 +31,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<result property="enabled" column="enabled" />
|
||||
<result property="sortOrder" column="sort_order" />
|
||||
<result property="detectItemId" column="detect_item_id" />
|
||||
<result property="chineseCorpus" column="chinese_corpus" />
|
||||
<result property="englishCorpus" column="english_corpus" />
|
||||
<result property="configStatus" column="config_status" />
|
||||
<result property="mapId" column="map_id" />
|
||||
<result property="mapName" column="map_name" />
|
||||
@ -37,7 +41,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectInspectionWaypointVo">
|
||||
select id, create_by, create_time, update_by, update_time, remark, waypoint_name, enabled, sort_order, detect_item_id, map_id from inspection_waypoint
|
||||
select id, create_by, create_time, update_by, update_time, remark, waypoint_name, enabled,
|
||||
sort_order, detect_item_id, chinese_corpus, english_corpus, map_id
|
||||
from inspection_waypoint
|
||||
</sql>
|
||||
|
||||
<select id="selectInspectionWaypointById" parameterType="String" resultMap="InspectionWaypointResult">
|
||||
@ -58,6 +64,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="enabled != null">enabled,</if>
|
||||
<if test="sortOrder != null">sort_order,</if>
|
||||
<if test="detectItemId != null">detect_item_id,</if>
|
||||
<if test="chineseCorpus != null">chinese_corpus,</if>
|
||||
<if test="englishCorpus != null">english_corpus,</if>
|
||||
<if test="mapId != null">map_id,</if>
|
||||
</trim>
|
||||
<trim prefix="values (" suffix=")" suffixOverrides=",">
|
||||
@ -71,6 +79,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="enabled != null">#{enabled},</if>
|
||||
<if test="sortOrder != null">#{sortOrder},</if>
|
||||
<if test="detectItemId != null">#{detectItemId},</if>
|
||||
<if test="chineseCorpus != null">#{chineseCorpus},</if>
|
||||
<if test="englishCorpus != null">#{englishCorpus},</if>
|
||||
<if test="mapId != null">#{mapId},</if>
|
||||
</trim>
|
||||
</insert>
|
||||
@ -87,6 +97,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
<if test="enabled != null">enabled = #{enabled},</if>
|
||||
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
|
||||
<if test="detectItemId != null">detect_item_id = #{detectItemId},</if>
|
||||
<if test="chineseCorpus != null">chinese_corpus = #{chineseCorpus},</if>
|
||||
<if test="englishCorpus != null">english_corpus = #{englishCorpus},</if>
|
||||
<if test="mapId != null">map_id = #{mapId},</if>
|
||||
</trim>
|
||||
where id = #{id}
|
||||
@ -105,7 +117,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
|
||||
<select id="selectInspectionWaypointVoList" parameterType="InspectionWaypoint" resultMap="InspectionWaypointVoResult">
|
||||
select w.id, w.create_by, w.create_time, w.update_by, w.update_time, w.remark,
|
||||
w.waypoint_name, w.enabled, w.sort_order, w.detect_item_id, w.map_id, m.map_name as map_name,
|
||||
w.waypoint_name, w.enabled, w.sort_order, w.detect_item_id,
|
||||
w.chinese_corpus, w.english_corpus, w.map_id, m.map_name as map_name,
|
||||
d.is_deploy as config_status,
|
||||
u1.nick_name as create_by_name, u2.nick_name as update_by_name
|
||||
from inspection_waypoint w
|
||||
|
||||
@ -87,6 +87,7 @@ public enum ActionEnum {
|
||||
GENERATE_ADVANCED_AUDIO("LLM", "GENERATE_ADVANCED_AUDIO", "tts语音合成"),
|
||||
AI_AGENT_PLATFORM("LLM", "AI_AGENT_PLATFORM", "商道智能体"),
|
||||
INSPECTION_METER_RECOGNIZE("LLM", "INSPECTION_METER_RECOGNIZE", "巡检仪表读数识别"),
|
||||
INSPECTION_WAYPOINT_CORPUS("LLM", "INSPECTION_WAYPOINT_CORPUS", "获取巡检点位播放语料"),
|
||||
AI_TTS("LLM", "AI_TTS", "tts语音播放"),
|
||||
GET_CURRENT_PAGE("LLM", "GET_CURRENT_PAGE", "获取当前页面名称"),
|
||||
AUDIO_EVENT_CLASSIFY("LLM", "AUDIO_EVENT_CLASSIFY", "声音类型检测"),
|
||||
|
||||
@ -10,11 +10,11 @@ import com.cmvr.edge.client.model.EdgeCommonVO;
|
||||
import com.cmvr.edge.client.model.camera.EdgeCameraRgbdSnapshot;
|
||||
import com.cmvr.edge.client.service.EdgeArmService;
|
||||
import com.cmvr.edge.client.service.EdgeCameraService;
|
||||
import com.cmvr.llm.analysis.MediaAnalysisClient;
|
||||
import com.cmvr.test.enums.ActionEnum;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteMessage;
|
||||
import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||
import com.cmvr.test.flow.runtime.operator.FlowMediaParamResolver;
|
||||
import com.cmvr.test.flow.runtime.operator.llm.LLMMediaAnalysisOperateService;
|
||||
import com.cmvr.test.vision.domain.VisionCalibrationProfile;
|
||||
import com.cmvr.test.vision.math.VisionCalibrationMath;
|
||||
import com.cmvr.test.vision.service.VisionCalibrationProfileService;
|
||||
@ -28,7 +28,6 @@ import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ -36,7 +35,7 @@ import java.util.UUID;
|
||||
public class VisionLocateOperateService implements EdgeOperateService {
|
||||
private final EdgeCameraService cameraService;
|
||||
private final EdgeArmService armService;
|
||||
private final MediaAnalysisClient mediaAnalysisClient;
|
||||
private final LLMMediaAnalysisOperateService imageAnalysisService;
|
||||
private final VisionCalibrationProfileService calibrationProfileService;
|
||||
|
||||
@Override
|
||||
@ -135,7 +134,7 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
} else {
|
||||
throw new GlobalException("mountType只支持EYE_IN_HAND或EYE_TO_HAND");
|
||||
}
|
||||
double[] objectPoint = VisionGeometry.transform(baseToCamera,
|
||||
double[] rgbdObjectPoint = VisionGeometry.transform(baseToCamera,
|
||||
surface.cameraPoint[0], surface.cameraPoint[1], surface.cameraPoint[2]);
|
||||
double[] surfaceNormal = normalize(rotate(baseToCamera, surface.normalCamera));
|
||||
double approachMm = input.getDouble("approachDistanceMm") == null
|
||||
@ -144,13 +143,23 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
approachMm = Math.max(1.0, Math.min(20.0, approachMm));
|
||||
double pressMm = Math.max(-20.0, Math.min(20.0, doubleValue(input, "pressDistanceMm", 0.0)));
|
||||
double[][] currentTcp = VisionGeometry.multiply(currentFlange, flangeToTcp);
|
||||
// The contact direction is the only authoritative click direction.
|
||||
// Build it once at the contact point, then change height only for the
|
||||
// approach pose. Re-solving the approach orientation independently is
|
||||
// unsafe when the fitted depth normal contains small fluctuations.
|
||||
double[] toolZ = scale(surfaceNormal, -1.0);
|
||||
// A reflective screen does not provide a stable depth-plane normal.
|
||||
// Rebuilding the full tool orientation from that noisy normal made the
|
||||
// physical tool tilt and caused equivalent Euler representations to
|
||||
// jump in rx/rz. Tool calibration already records the operator-verified
|
||||
// perpendicular pose; use it as the authoritative click orientation.
|
||||
// Legacy/manual configurations have no such profile, so keep their
|
||||
// current TCP orientation and update position only.
|
||||
StableToolOrientation stableOrientation = stableToolOrientation(
|
||||
currentTcp, flangeToTcp, toolProfile);
|
||||
double[][] orientedTcp = stableOrientation.tcpPose;
|
||||
double[] toolZ = normalize(new double[]{orientedTcp[0][2], orientedTcp[1][2], orientedTcp[2][2]});
|
||||
TargetPosition targetPosition = targetPosition(
|
||||
baseToCamera, snapshot, pixel.x, pixel.y, rgbdObjectPoint,
|
||||
surface.samplesCamera, stableOrientation, input.getDouble("planeOffsetMm"));
|
||||
double[] objectPoint = targetPosition.point;
|
||||
double[] contactPosition = add(objectPoint, scale(toolZ, pressMm / 1000.0));
|
||||
double[][] contactTcp = targetToolPose(currentTcp, toolZ, contactPosition);
|
||||
double[][] contactTcp = translatedPose(orientedTcp, contactPosition);
|
||||
double[] approachPosition = add(contactPosition, scale(toolZ, -(approachMm + pressMm) / 1000.0));
|
||||
double[][] approachTcp = translatedPose(contactTcp, approachPosition);
|
||||
double[][] tcpToFlange = VisionGeometry.rigidInverse(flangeToTcp);
|
||||
@ -170,8 +179,15 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
output.put("confidence", pixel.confidence);
|
||||
output.put("depth", depth);
|
||||
output.put("cameraPoint", point3(surface.cameraPoint[0], surface.cameraPoint[1], depth));
|
||||
output.put("rgbdObjectPoint", point3(
|
||||
rgbdObjectPoint[0], rgbdObjectPoint[1], rgbdObjectPoint[2]));
|
||||
output.put("objectPoint", point3(objectPoint[0], objectPoint[1], objectPoint[2]));
|
||||
output.put("positionSource", targetPosition.source);
|
||||
output.put("depthResidualMm", targetPosition.depthResidualMm);
|
||||
output.put("planeOffsetMm", input.getDouble("planeOffsetMm"));
|
||||
output.put("surfaceNormal", point3(surfaceNormal[0], surfaceNormal[1], surfaceNormal[2]));
|
||||
output.put("toolAxis", point3(toolZ[0], toolZ[1], toolZ[2]));
|
||||
output.put("orientationSource", stableOrientation.source);
|
||||
output.put("surfaceFitErrorMm", surface.rmse * 1000.0);
|
||||
output.put("approachTcpPose", poseJson(VisionGeometry.pose(approachTcp)));
|
||||
output.put("contactTcpPose", poseJson(VisionGeometry.pose(contactTcp)));
|
||||
@ -197,62 +213,81 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
.fluentPut("fy", snapshot.getFy())
|
||||
.fluentPut("cx", snapshot.getCx())
|
||||
.fluentPut("cy", snapshot.getCy()));
|
||||
log.info("视觉定位三维结果: pixel=({},{}), rgbdZ={}, targetZ={}, depthResidualMm={}, "
|
||||
+ "positionSource={}, orientationSource={}, contactFlangePose={}",
|
||||
pixel.x, pixel.y, rgbdObjectPoint[2], objectPoint[2], targetPosition.depthResidualMm,
|
||||
targetPosition.source, stableOrientation.source,
|
||||
output.getJSONObject("contactFlangePose"));
|
||||
return TaskNodeExecuteResult.success(output);
|
||||
}
|
||||
|
||||
private JSONObject analyze(TaskNodeExecuteMessage message, String imageUrl, String profileCode,
|
||||
String prompt, JSONObject input, String effectiveAnalysisMethod) {
|
||||
JSONObject options = new JSONObject();
|
||||
JSONObject analyze(TaskNodeExecuteMessage message, String imageUrl, String profileCode,
|
||||
String prompt, JSONObject input, String effectiveAnalysisMethod) {
|
||||
String analysisMethod = StringUtils.defaultIfBlank(effectiveAnalysisMethod, "TEXT_LOCATE").toUpperCase();
|
||||
options.put("prompt", "REFERENCE_MATCH".equals(analysisMethod)
|
||||
? "在大图中查找参考小图,返回目标中心坐标。"
|
||||
JSONObject analysisInput = new JSONObject();
|
||||
analysisInput.put("profileCode", profileCode);
|
||||
analysisInput.put("imageUrl", new JSONArray().fluentAdd(imageUrl));
|
||||
analysisInput.put("prompt", "REFERENCE_MATCH".equals(analysisMethod)
|
||||
? "在大图中查找小图,返回原图像素坐标 [x1,y1,x2,y2]"
|
||||
: ("TEXT_LOCATE".equals(analysisMethod) ? prompt : buildLocatePrompt(prompt)));
|
||||
// Reference matching is deterministic OpenCV matching; FAST/ACCURATE
|
||||
// only select model strategies for description-based analysis. Force
|
||||
// the same advertised mode as the generic precise-image component so
|
||||
// legacy nodes cannot silently diverge in their request metadata.
|
||||
options.put("analysisMode", "REFERENCE_MATCH".equals(analysisMethod)
|
||||
// 精确找图直接复用通用图片分析组件的 FAST + REFERENCE_MATCH 请求,
|
||||
// 不在视觉定位节点中维护第二套匹配实现或参数契约。
|
||||
analysisInput.put("analysisMode", "REFERENCE_MATCH".equals(analysisMethod)
|
||||
? "FAST"
|
||||
: StringUtils.defaultIfBlank(input.getString("analysisMode"), "FAST"));
|
||||
options.put("analysisMethod", analysisMethod);
|
||||
analysisInput.put("analysisMethod", analysisMethod);
|
||||
analysisInput.put("asynchronous", false);
|
||||
String referenceImageUrl = referenceImageUrl(input);
|
||||
if (StringUtils.isNotBlank(referenceImageUrl)) options.put("referenceImageUrl", referenceImageUrl);
|
||||
if (StringUtils.isNotBlank(referenceImageUrl)) {
|
||||
analysisInput.put("referenceImageUrl", referenceImageUrl);
|
||||
}
|
||||
JSONObject tuning = input.getJSONObject("analysisTuning");
|
||||
if (tuning == null) tuning = new JSONObject();
|
||||
tuning = tuning == null ? new JSONObject() : new JSONObject(tuning);
|
||||
if (input.getString("matchSelector") != null) tuning.put("matchSelector", input.getString("matchSelector"));
|
||||
if (input.get("matchIndex") != null) tuning.put("matchIndex", input.get("matchIndex"));
|
||||
if (input.getString("matchSort") != null) tuning.put("matchSort", input.getString("matchSort"));
|
||||
if (input.get("matchThreshold") != null) tuning.put("matchThreshold", input.get("matchThreshold"));
|
||||
// Keep precise-location matching consistent with the generic image
|
||||
// analysis component. Older visual-location nodes do not have an
|
||||
// analysisTuning object, so fill in the same stable defaults here.
|
||||
if (input.get("locateConfidenceThreshold") != null) {
|
||||
tuning.put("locateConfidenceThreshold", input.get("locateConfidenceThreshold"));
|
||||
} else if (input.get("confidenceThreshold") != null) {
|
||||
tuning.put("locateConfidenceThreshold", input.get("confidenceThreshold"));
|
||||
}
|
||||
// 旧视觉定位节点没有 analysisTuning,补齐通用图片分析组件当前的
|
||||
// 精确找图默认值,保证单独使用视觉定位时发出的匹配请求完全一致。
|
||||
if ("REFERENCE_MATCH".equals(analysisMethod)) {
|
||||
// Preserve small-icon detail for template matching. The analysis
|
||||
// service still caps this value for very large source images.
|
||||
if (!tuning.containsKey("maxWidth")) tuning.put("maxWidth", 1600);
|
||||
if (!tuning.containsKey("maxImages")) tuning.put("maxImages", 1);
|
||||
if (!tuning.containsKey("maxWidth")) tuning.put("maxWidth", 896);
|
||||
if (!tuning.containsKey("maxImages")) tuning.put("maxImages", 12);
|
||||
if (!tuning.containsKey("maxOutputTokens")) tuning.put("maxOutputTokens", 384);
|
||||
if (!tuning.containsKey("matchThreshold")) tuning.put("matchThreshold", 0.48D);
|
||||
if (!tuning.containsKey("matchSelector")) tuning.put("matchSelector", "BEST");
|
||||
if (!tuning.containsKey("matchIndex")) tuning.put("matchIndex", 1);
|
||||
if (!tuning.containsKey("matchSort")) tuning.put("matchSort", "ROW_MAJOR");
|
||||
if (!tuning.containsKey("locateConfidenceThreshold")) {
|
||||
tuning.put("locateConfidenceThreshold", 0.45D);
|
||||
}
|
||||
}
|
||||
if (!tuning.isEmpty()) options.put("tuning", tuning);
|
||||
JSONObject request = new JSONObject();
|
||||
request.put("requestId", "vision-locate-" + UUID.randomUUID());
|
||||
request.put("analysisType", "IMAGE_ANALYSIS");
|
||||
request.put("profileCode", profileCode);
|
||||
request.put("mediaUrl", imageUrl);
|
||||
request.put("mediaUrls", List.of(imageUrl));
|
||||
request.put("options", options);
|
||||
request.put("context", new JSONObject()
|
||||
.fluentPut("flowInstId", message.getInstId())
|
||||
.fluentPut("nodeId", message.getNodeId())
|
||||
.fluentPut("trial", message.isTrial()));
|
||||
JSONObject response = mediaAnalysisClient.analyze(request);
|
||||
JSONObject result = response.getJSONObject("result");
|
||||
if (result == null) throw new GlobalException("图片分析未返回目标结果");
|
||||
return result;
|
||||
if (!tuning.isEmpty()) analysisInput.put("analysisTuning", tuning);
|
||||
|
||||
TaskNodeExecuteMessage analysisMessage = imageAnalysisMessage(message, analysisInput);
|
||||
TaskNodeExecuteResult analysisResult = imageAnalysisService.execute(analysisMessage);
|
||||
if (analysisResult == null || !analysisResult.isSuccess()
|
||||
|| analysisResult.getOutputParams() == null) {
|
||||
String error = analysisResult == null ? null : analysisResult.getErrorMsg();
|
||||
throw new GlobalException(StringUtils.defaultIfBlank(error, "图片分析未返回目标结果"));
|
||||
}
|
||||
return analysisResult.getOutputParams();
|
||||
}
|
||||
|
||||
private TaskNodeExecuteMessage imageAnalysisMessage(TaskNodeExecuteMessage source, JSONObject input) {
|
||||
TaskNodeExecuteMessage message = new TaskNodeExecuteMessage();
|
||||
message.setInstId(source.getInstId());
|
||||
message.setTaskId(source.getTaskId());
|
||||
message.setItemId(source.getItemId());
|
||||
message.setNodeId(source.getNodeId());
|
||||
message.setTrial(source.isTrial());
|
||||
message.setAction(ActionEnum.IMAGE_ANALYZE);
|
||||
message.setInputParams(input);
|
||||
return message;
|
||||
}
|
||||
|
||||
private String referenceImageUrl(JSONObject input) {
|
||||
@ -450,6 +485,12 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
|
||||
private SurfacePoint fitSurface(EdgeCameraRgbdSnapshot snapshot, double centerX, double centerY,
|
||||
int radius, Double configuredScale) {
|
||||
// Position is tied to the requested image pixel. A local plane fit is
|
||||
// useful as diagnostics, but its slope must not move the target point.
|
||||
double centerDepth = medianDepth(
|
||||
snapshot, centerX, centerY, Math.max(radius, 8), configuredScale);
|
||||
double[] center = {(centerX - snapshot.getCx()) * centerDepth / snapshot.getFx(),
|
||||
(centerY - snapshot.getCy()) * centerDepth / snapshot.getFy(), centerDepth};
|
||||
List<double[]> points = new ArrayList<>();
|
||||
int step = Math.max(1, radius / 6);
|
||||
for (int y = (int) Math.round(centerY) - radius; y <= (int) Math.round(centerY) + radius; y += step) {
|
||||
@ -468,15 +509,14 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
// Reflective phone screens often contain only a few valid stereo
|
||||
// pixels. Keep the target depth and use the camera optical normal
|
||||
// as a conservative fallback instead of failing the whole node.
|
||||
double depth = medianDepth(snapshot, centerX, centerY, Math.max(radius, 8), configuredScale);
|
||||
double[] center = {(centerX - snapshot.getCx()) * depth / snapshot.getFx(),
|
||||
(centerY - snapshot.getCy()) * depth / snapshot.getFy(), depth};
|
||||
return new SurfacePoint(center, new double[]{0, 0, -1}, 0.0);
|
||||
return new SurfacePoint(center, new double[]{0, 0, -1}, 0.0, points);
|
||||
}
|
||||
List<Double> depths = points.stream().map(point -> point[2]).sorted().toList();
|
||||
double median = depths.get(depths.size() / 2);
|
||||
points.removeIf(point -> Math.abs(point[2] - median) > 0.02);
|
||||
if (points.size() < 15) return fallbackSurface(snapshot, centerX, centerY, radius, configuredScale);
|
||||
if (points.size() < 15) {
|
||||
return new SurfacePoint(center, new double[]{0, 0, -1}, 0.0, points);
|
||||
}
|
||||
|
||||
double[][] normal = new double[3][3];
|
||||
double[] rhs = new double[3];
|
||||
@ -491,24 +531,13 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
double length = Math.sqrt(solution[0] * solution[0] + solution[1] * solution[1] + 1);
|
||||
double[] planeNormal = {solution[0] / length, solution[1] / length, -1 / length};
|
||||
double planeD = solution[2] / length;
|
||||
double[] ray = {(centerX - snapshot.getCx()) / snapshot.getFx(),
|
||||
(centerY - snapshot.getCy()) / snapshot.getFy(), 1};
|
||||
double rayScale = -planeD / dot(planeNormal, ray);
|
||||
double[] center = scale(ray, rayScale);
|
||||
double squaredError = 0;
|
||||
for (double[] point : points) {
|
||||
double error = dot(planeNormal, point) + planeD;
|
||||
squaredError += error * error;
|
||||
}
|
||||
return new SurfacePoint(center, planeNormal, Math.sqrt(squaredError / points.size()));
|
||||
}
|
||||
|
||||
private SurfacePoint fallbackSurface(EdgeCameraRgbdSnapshot snapshot, double centerX, double centerY,
|
||||
int radius, Double configuredScale) {
|
||||
double depth = medianDepth(snapshot, centerX, centerY, Math.max(radius, 8), configuredScale);
|
||||
double[] center = {(centerX - snapshot.getCx()) * depth / snapshot.getFx(),
|
||||
(centerY - snapshot.getCy()) * depth / snapshot.getFy(), depth};
|
||||
return new SurfacePoint(center, new double[]{0, 0, -1}, 0.0);
|
||||
return new SurfacePoint(
|
||||
center, planeNormal, Math.sqrt(squaredError / points.size()), points);
|
||||
}
|
||||
|
||||
private double[] solve3(double[][] source, double[] right) {
|
||||
@ -537,23 +566,119 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
return result;
|
||||
}
|
||||
|
||||
private double[][] targetToolPose(double[][] currentTcp, double[] targetZ, double[] position) {
|
||||
targetZ = normalize(targetZ);
|
||||
double[] xSeed = {currentTcp[0][0], currentTcp[1][0], currentTcp[2][0]};
|
||||
double[] x = subtract(xSeed, scale(targetZ, dot(xSeed, targetZ)));
|
||||
if (length(x) < 1e-8) {
|
||||
xSeed = new double[]{currentTcp[0][1], currentTcp[1][1], currentTcp[2][1]};
|
||||
x = subtract(xSeed, scale(targetZ, dot(xSeed, targetZ)));
|
||||
StableToolOrientation stableToolOrientation(double[][] currentTcp, double[][] flangeToTcp,
|
||||
VisionCalibrationProfile toolProfile) {
|
||||
if (toolProfile == null || StringUtils.isBlank(toolProfile.getMetadataJson())) {
|
||||
return new StableToolOrientation(currentTcp, "CURRENT_TCP", null, null);
|
||||
}
|
||||
x = normalize(x);
|
||||
double[] y = normalize(cross(targetZ, x));
|
||||
x = normalize(cross(y, targetZ));
|
||||
double[][] result = new double[4][4];
|
||||
result[3][3] = 1;
|
||||
for (int row = 0; row < 3; row++) {
|
||||
result[row][0] = x[row]; result[row][1] = y[row]; result[row][2] = targetZ[row]; result[row][3] = position[row];
|
||||
try {
|
||||
JSONObject metadata = JSON.parseObject(toolProfile.getMetadataJson());
|
||||
Object safeDirectionPose = metadata == null ? null : metadata.get("safeDirectionPose");
|
||||
if (safeDirectionPose == null) {
|
||||
return new StableToolOrientation(currentTcp, "CURRENT_TCP", null, null);
|
||||
}
|
||||
double[][] safeFlange = VisionGeometry.parseMatrix(
|
||||
safeDirectionPose, "toolCalibration.safeDirectionPose");
|
||||
double[][] safeTcp = VisionGeometry.multiply(safeFlange, flangeToTcp);
|
||||
double[] planeNormal = vector3(metadata.get("planeNormalBase"));
|
||||
double[] planePoint = planeNormal == null ? null
|
||||
: new double[]{safeTcp[0][3], safeTcp[1][3], safeTcp[2][3]};
|
||||
return new StableToolOrientation(
|
||||
safeTcp, "TOOL_CALIBRATION_SAFE_DIRECTION", planePoint, planeNormal);
|
||||
} catch (RuntimeException exception) {
|
||||
log.warn("工具标定安全方向无效,保持当前TCP姿态: profileId={}",
|
||||
toolProfile.getId(), exception);
|
||||
return new StableToolOrientation(
|
||||
currentTcp, "CURRENT_TCP_INVALID_CALIBRATION_FALLBACK", null, null);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TargetPosition targetPosition(double[][] baseToCamera, EdgeCameraRgbdSnapshot snapshot,
|
||||
double pixelX, double pixelY, double[] rgbdObjectPoint,
|
||||
List<double[]> samplesCamera,
|
||||
StableToolOrientation orientation,
|
||||
Double configuredPlaneOffsetMm) {
|
||||
if (orientation.planeNormal == null) {
|
||||
return new TargetPosition(rgbdObjectPoint, "RGBD_MEDIAN_DEPTH", 0.0);
|
||||
}
|
||||
double[] rayCamera = {(pixelX - snapshot.getCx()) / snapshot.getFx(),
|
||||
(pixelY - snapshot.getCy()) / snapshot.getFy(), 1.0};
|
||||
double[] rayOrigin = {baseToCamera[0][3], baseToCamera[1][3], baseToCamera[2][3]};
|
||||
double[] rayDirection = rotate(baseToCamera, rayCamera);
|
||||
double[] planeNormal = normalize(orientation.planeNormal);
|
||||
double denominator = dot(planeNormal, rayDirection);
|
||||
if (Math.abs(denominator) < 1e-8) {
|
||||
throw new GlobalException("目标像素射线与标定平面平行,无法计算稳定坐标");
|
||||
}
|
||||
double planeDistance;
|
||||
String source;
|
||||
if (configuredPlaneOffsetMm != null && orientation.planePoint != null) {
|
||||
// A known height offset is fully deterministic and provides the
|
||||
// best repeatability for fixtures with discrete height settings.
|
||||
planeDistance = dot(planeNormal, orientation.planePoint)
|
||||
+ configuredPlaneOffsetMm / 1000.0;
|
||||
source = "TOOL_CALIBRATION_PLANE_OFFSET";
|
||||
} else {
|
||||
List<Double> distances = new ArrayList<>();
|
||||
if (samplesCamera != null) {
|
||||
for (double[] cameraPoint : samplesCamera) {
|
||||
double[] pointBase = VisionGeometry.transform(
|
||||
baseToCamera, cameraPoint[0], cameraPoint[1], cameraPoint[2]);
|
||||
double distance = dot(planeNormal, pointBase);
|
||||
if (Double.isFinite(distance)) distances.add(distance);
|
||||
}
|
||||
}
|
||||
if (distances.size() < 9) {
|
||||
// Height cannot be inferred robustly, but the median depth at
|
||||
// the target pixel still adapts to a moved parallel surface.
|
||||
return new TargetPosition(rgbdObjectPoint, "RGBD_MEDIAN_DEPTH", 0.0);
|
||||
}
|
||||
planeDistance = robustMedian(distances);
|
||||
source = "RGBD_PARALLEL_PLANE";
|
||||
}
|
||||
double rayScale = (planeDistance - dot(planeNormal, rayOrigin)) / denominator;
|
||||
if (!Double.isFinite(rayScale) || rayScale <= 0) {
|
||||
throw new GlobalException("标定平面位于相机目标射线后方,请重新检查工具标定");
|
||||
}
|
||||
double[] point = add(rayOrigin, scale(rayDirection, rayScale));
|
||||
double residualMm = dot(planeNormal, subtract(rgbdObjectPoint, point)) * 1000.0;
|
||||
return new TargetPosition(point, source, residualMm);
|
||||
}
|
||||
|
||||
private double robustMedian(List<Double> values) {
|
||||
List<Double> sorted = new ArrayList<>(values);
|
||||
Collections.sort(sorted);
|
||||
double median = sorted.get(sorted.size() / 2);
|
||||
List<Double> deviations = sorted.stream()
|
||||
.map(value -> Math.abs(value - median))
|
||||
.sorted()
|
||||
.toList();
|
||||
double mad = deviations.get(deviations.size() / 2);
|
||||
if (mad < 1e-9) return median;
|
||||
double limit = Math.max(0.001, mad * 3.5);
|
||||
List<Double> inliers = sorted.stream()
|
||||
.filter(value -> Math.abs(value - median) <= limit)
|
||||
.toList();
|
||||
return inliers.get(inliers.size() / 2);
|
||||
}
|
||||
|
||||
private double[] vector3(Object value) {
|
||||
JSONArray array;
|
||||
try {
|
||||
array = value instanceof String
|
||||
? JSON.parseArray(((String) value).trim())
|
||||
: JSON.parseArray(JSON.toJSONString(value));
|
||||
} catch (RuntimeException exception) {
|
||||
return null;
|
||||
}
|
||||
if (array == null || array.size() != 3) return null;
|
||||
double[] result = new double[3];
|
||||
for (int index = 0; index < 3; index++) {
|
||||
Double number = array.getDouble(index);
|
||||
if (number == null || !Double.isFinite(number)) return null;
|
||||
result[index] = number;
|
||||
}
|
||||
return normalize(result);
|
||||
}
|
||||
|
||||
/** Keep the contact orientation exactly while changing only TCP height. */
|
||||
@ -583,7 +708,6 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
private double[] subtract(double[] left, double[] right) { return new double[]{left[0]-right[0], left[1]-right[1], left[2]-right[2]}; }
|
||||
private double[] scale(double[] value, double factor) { return new double[]{value[0]*factor, value[1]*factor, value[2]*factor}; }
|
||||
private double dot(double[] left, double[] right) { return left[0]*right[0]+left[1]*right[1]+left[2]*right[2]; }
|
||||
private double[] cross(double[] left, double[] right) { return new double[]{left[1]*right[2]-left[2]*right[1], left[2]*right[0]-left[0]*right[2], left[0]*right[1]-left[1]*right[0]}; }
|
||||
private double length(double[] value) { return Math.sqrt(dot(value, value)); }
|
||||
private double[] normalize(double[] value) { double length = length(value); if (length < 1e-10) throw new GlobalException("无法确定目标表面方向"); return scale(value, 1/length); }
|
||||
|
||||
@ -656,6 +780,14 @@ public class VisionLocateOperateService implements EdgeOperateService {
|
||||
private record TargetPixel(double x, double y, double confidence) {
|
||||
}
|
||||
|
||||
private record SurfacePoint(double[] cameraPoint, double[] normalCamera, double rmse) {
|
||||
private record SurfacePoint(double[] cameraPoint, double[] normalCamera, double rmse,
|
||||
List<double[]> samplesCamera) {
|
||||
}
|
||||
|
||||
record StableToolOrientation(double[][] tcpPose, String source,
|
||||
double[] planePoint, double[] planeNormal) {
|
||||
}
|
||||
|
||||
record TargetPosition(double[] point, String source, double depthResidualMm) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ public class FlowActionRequestVO {
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
@ApiModelProperty(value = "动作编码,例如 DEVICE_EXECUTE_JSON_COMMAND、INSPECTION_METER_RECOGNIZE",
|
||||
@ApiModelProperty(value = "动作编码,例如 DEVICE_EXECUTE_JSON_COMMAND、INSPECTION_METER_RECOGNIZE、INSPECTION_WAYPOINT_CORPUS",
|
||||
required = true, example = "DEVICE_EXECUTE_JSON_COMMAND")
|
||||
private String action;
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import com.cmvr.test.flow.runtime.message.TaskNodeExecuteResult;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
@ -20,7 +21,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void mapsAudioArrayInputAndReturnsAnalysisMetadata() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -31,7 +32,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("result", new JSONObject()
|
||||
.fluentPut("label", "POWER_ON")
|
||||
.fluentPut("matched", true));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "aima.power_state.v1")
|
||||
@ -52,7 +53,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void comparesRecognizedAudioWithExpectedType() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -62,7 +63,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("label", "POWER_OFF")
|
||||
.fluentPut("labelName", "关机")
|
||||
.fluentPut("matched", true));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "aima.power_state.v1")
|
||||
@ -80,14 +81,14 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
|
||||
@Test
|
||||
public void passesBroadVehicleSoundTargetWhenConcreteLabelIsAmbiguous() {
|
||||
MediaAnalysisClient client = request -> new JSONObject()
|
||||
MediaAnalysisClient client = client(request -> new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
.fluentPut("analysisType", "AUDIO_CLASSIFICATION")
|
||||
.fluentPut("profileCode", "aima.power_state.v1")
|
||||
.fluentPut("result", new JSONObject()
|
||||
.fluentPut("label", "UNKNOWN")
|
||||
.fluentPut("matched", false)
|
||||
.fluentPut("vehicleSoundDetected", true));
|
||||
.fluentPut("vehicleSoundDetected", true)));
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "aima.power_state.v1")
|
||||
@ -104,7 +105,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void forwardsVideoInstruction() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -113,7 +114,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("timingMs", 30L)
|
||||
.fluentPut("model", new JSONObject().fluentPut("name", "qwen3-vl:32b"))
|
||||
.fluentPut("result", new JSONObject().fluentPut("summary", "ok"));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "aima.power_video.v1")
|
||||
@ -146,7 +147,8 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
|
||||
@Test(expected = GlobalException.class)
|
||||
public void rejectsOutOfRangeVideoTuning() {
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(request -> new JSONObject());
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(
|
||||
client(request -> new JSONObject()));
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "aima.power_video.v1")
|
||||
.fluentPut("videoUrl", "https://files.example/video.mp4")
|
||||
@ -159,7 +161,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void forwardsMultipleImagesAndReferenceImage() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -168,7 +170,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("result", new JSONObject()
|
||||
.fluentPut("result", new JSONObject().fluentPut("x", 120).fluentPut("y", 80))
|
||||
.fluentPut("resultText", "{\"x\":120,\"y\":80}"));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "common.image_analysis.v1")
|
||||
@ -210,7 +212,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void forwardsDescriptionLocationMethodAndThreshold() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -219,7 +221,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("result", new JSONObject()
|
||||
.fluentPut("result", new JSONArray().fluentAdd(10).fluentAdd(20).fluentAdd(30).fluentAdd(40))
|
||||
.fluentPut("matched", true));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "common.image_analysis.v1")
|
||||
@ -241,7 +243,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
@Test
|
||||
public void restoresStaticImageUrlArraySerializedByLegacyFlowParamModel() {
|
||||
AtomicReference<JSONObject> captured = new AtomicReference<>();
|
||||
MediaAnalysisClient client = request -> {
|
||||
MediaAnalysisClient client = client(request -> {
|
||||
captured.set(request);
|
||||
return new JSONObject()
|
||||
.fluentPut("status", "SUCCEEDED")
|
||||
@ -250,7 +252,7 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
.fluentPut("result", new JSONObject()
|
||||
.fluentPut("result", "图片正常")
|
||||
.fluentPut("resultText", "图片正常"));
|
||||
};
|
||||
});
|
||||
LLMMediaAnalysisOperateService service = new LLMMediaAnalysisOperateService(client);
|
||||
JSONObject input = new JSONObject()
|
||||
.fluentPut("profileCode", "common.image_analysis.v1")
|
||||
@ -281,4 +283,18 @@ public class LLMMediaAnalysisOperateServiceTest {
|
||||
message.setInputParams(input);
|
||||
return message;
|
||||
}
|
||||
|
||||
private MediaAnalysisClient client(Function<JSONObject, JSONObject> analyze) {
|
||||
return new MediaAnalysisClient() {
|
||||
@Override
|
||||
public JSONObject analyze(JSONObject request) {
|
||||
return analyze.apply(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject addAudioReference(JSONObject request) {
|
||||
throw new UnsupportedOperationException("Not used by this test");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +36,8 @@ CREATE TABLE `inspection_waypoint` (
|
||||
`enabled` char(1) DEFAULT '0' COMMENT '是否启用(0启用 1禁用)',
|
||||
`sort_order` int(11) DEFAULT '0' COMMENT '排序号',
|
||||
`detect_item_id` varchar(64) DEFAULT NULL COMMENT '检测项ID',
|
||||
`chinese_corpus` text DEFAULT NULL COMMENT '该点位可播放的中文语料',
|
||||
`english_corpus` text DEFAULT NULL COMMENT '该点位可播放的英文语料',
|
||||
`map_id` varchar(64) DEFAULT NULL COMMENT '地图ID',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检点位表';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user