Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1c4c78510 | |||
| 011854c42a | |||
| 19b63fd419 | |||
| 5b4ff89ef0 | |||
| 1874c4a5bb | |||
| b0397365b2 | |||
| a3a6bf7225 | |||
| 580d61acae | |||
| 703e548c31 | |||
| b3d39c3492 | |||
| bc626a0ca8 | |||
| 84c1c5f16d | |||
| 030433cc92 | |||
| cdd0db95ff | |||
| 82b7c32147 | |||
| 77f6a32eda | |||
| 7c06cd2def | |||
| 0f24209e13 | |||
| eb8d7a1491 | |||
| d51d2b0013 | |||
| 06c39c5ab4 | |||
| 581ed2f3df | |||
| 03348a91ac | |||
| 4afbadffd1 | |||
| cf15ed740f | |||
| c443404bae | |||
| 92213ffd0d | |||
| 54d336a0b1 | |||
| 0b6554f139 | |||
| 4943bc16b7 | |||
| 3777ad2d53 | |||
| de4a7121db | |||
| 43d0e7fa89 | |||
| 05c41a9a21 | |||
| 01b12bb246 | |||
| 2ac84ecc85 | |||
| 8905de6bab | |||
| 15e3e56856 | |||
| 2d7cf17a3b | |||
| 2d14954bd3 | |||
| 119d03a02b | |||
| e6e776357d | |||
| 5c56779f9f | |||
| d7c027b732 | |||
| db0eac5d49 | |||
| 31825a43b9 | |||
| 2e4b2d714b | |||
| 375fb832f0 | |||
| e3cfac5f09 | |||
| 14f41321c7 | |||
| 2d56f42d28 | |||
| 6abe391450 | |||
| 1546aec567 | |||
| eb6ee48a5e | |||
| 80c86e2ed7 | |||
| 3cfd115d72 | |||
| ef83a7637a | |||
| 3f14d14393 | |||
| 04da9cd798 | |||
| d59550c492 | |||
| e233f0fd96 | |||
| ed0fe92cbe | |||
| 9c639f27cd | |||
| 843c4d67a6 | |||
| a9d56ebfbd | |||
| 346f4d9a9b | |||
| 98d91efc19 | |||
| 94f0649af8 | |||
| ad719d157b | |||
| 12938764b1 | |||
| 7447463c1a | |||
| 9634e42396 | |||
| e0eec42a2f | |||
| 82465e6510 | |||
| baef619bd4 | |||
| 1ad33cc3e6 | |||
| 1fb789190c | |||
| 5960f28bcf | |||
| 7441526ed9 |
@@ -29,6 +29,7 @@ bld/
|
||||
[Ll]ogs/
|
||||
lib/
|
||||
XP.ImageProcessing/
|
||||
XP.ImageProcessing.SmokeTest/
|
||||
ImageProcessing.sln
|
||||
|
||||
# 排除 Libs 目录中的 DLL 和 PDB 文件(但保留目录结构)
|
||||
@@ -67,3 +68,4 @@ XplorePlane/data/
|
||||
XplorePlane.Tests/bin_codex/
|
||||
|
||||
DataBase/XP.db
|
||||
XplorePlane.Tests/TestResults/
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<Config>
|
||||
<Group ID="WriteCommon" DBNumber="31">
|
||||
<Signal Name="SoftLive" Type="byte" StartAddr="0" IndexOrLength="" Remark="软件心跳,01周期变化表示PLC存活" />
|
||||
<Signal Name="EmergencyStop" Type="byte" StartAddr="5" IndexOrLength="" Remark="急停,0:缺省,1:触发急停" />
|
||||
<Signal Name="MC_SourceZ_Target" Type="single" StartAddr="10" IndexOrLength="" Remark="射线源Z目标位置" />
|
||||
<Signal Name="MC_SourceZ_Speed" Type="single" StartAddr="14" IndexOrLength="" Remark="射线源Z运动速度" />
|
||||
<Signal Name="MC_SourceZ_JogPos" Type="byte" StartAddr="18" IndexOrLength="" Remark="射线源Z正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_SourceZ_JogNeg" Type="byte" StartAddr="19" IndexOrLength="" Remark="射线源Z反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_SourceZ_Home" Type="byte" StartAddr="20" IndexOrLength="" Remark="射线源Z回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_SourceZ_Stop" Type="byte" StartAddr="21" IndexOrLength="" Remark="射线源Z停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_DetZ_Target" Type="single" StartAddr="22" IndexOrLength="" Remark="探测器Z目标位置" />
|
||||
<Signal Name="MC_DetZ_Speed" Type="single" StartAddr="26" IndexOrLength="" Remark="探测器Z运动速度" />
|
||||
<Signal Name="MC_DetZ_JogPos" Type="byte" StartAddr="30" IndexOrLength="" Remark="探测器Z正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_DetZ_JogNeg" Type="byte" StartAddr="31" IndexOrLength="" Remark="探测器Z反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_DetZ_Home" Type="byte" StartAddr="32" IndexOrLength="" Remark="探测器Z回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_DetZ_Stop" Type="byte" StartAddr="33" IndexOrLength="" Remark="探测器Z停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_StageX_Target" Type="single" StartAddr="34" IndexOrLength="" Remark="载物台X目标位置" />
|
||||
<Signal Name="MC_StageX_Speed" Type="single" StartAddr="38" IndexOrLength="" Remark="载物台X运动速度" />
|
||||
<Signal Name="MC_StageX_JogPos" Type="byte" StartAddr="42" IndexOrLength="" Remark="载物台X正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_StageX_JogNeg" Type="byte" StartAddr="43" IndexOrLength="" Remark="载物台X反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_StageX_Home" Type="byte" StartAddr="44" IndexOrLength="" Remark="载物台X回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_StageX_Stop" Type="byte" StartAddr="45" IndexOrLength="" Remark="载物台X停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_StageY_Target" Type="single" StartAddr="46" IndexOrLength="" Remark="载物台Y目标位置" />
|
||||
<Signal Name="MC_StageY_Speed" Type="single" StartAddr="50" IndexOrLength="" Remark="载物台Y运动速度" />
|
||||
<Signal Name="MC_StageY_JogPos" Type="byte" StartAddr="54" IndexOrLength="" Remark="载物台Y正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_StageY_JogNeg" Type="byte" StartAddr="55" IndexOrLength="" Remark="载物台Y反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_StageY_Home" Type="byte" StartAddr="56" IndexOrLength="" Remark="载物台Y回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_StageY_Stop" Type="byte" StartAddr="57" IndexOrLength="" Remark="载物台Y停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_DetSwing_Target" Type="single" StartAddr="58" IndexOrLength="" Remark="探测器摆动目标角度" />
|
||||
<Signal Name="MC_DetSwing_Speed" Type="single" StartAddr="62" IndexOrLength="" Remark="探测器摆动运动速度" />
|
||||
<Signal Name="MC_DetSwing_JogPos" Type="byte" StartAddr="66" IndexOrLength="" Remark="探测器摆动正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_DetSwing_JogNeg" Type="byte" StartAddr="67" IndexOrLength="" Remark="探测器摆动反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_DetSwing_Home" Type="byte" StartAddr="68" IndexOrLength="" Remark="探测器摆动回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_DetSwing_Stop" Type="byte" StartAddr="69" IndexOrLength="" Remark="探测器摆动停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_StageRot_Target" Type="single" StartAddr="70" IndexOrLength="" Remark="载物台旋转目标角度" />
|
||||
<Signal Name="MC_StageRot_Speed" Type="single" StartAddr="74" IndexOrLength="" Remark="载物台旋转运动速度" />
|
||||
<Signal Name="MC_StageRot_JogPos" Type="byte" StartAddr="78" IndexOrLength="" Remark="载物台旋转正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_StageRot_JogNeg" Type="byte" StartAddr="79" IndexOrLength="" Remark="载物台旋转反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_StageRot_Home" Type="byte" StartAddr="80" IndexOrLength="" Remark="载物台旋转回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_StageRot_Stop" Type="byte" StartAddr="81" IndexOrLength="" Remark="载物台旋转停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_FixRot_Target" Type="single" StartAddr="82" IndexOrLength="" Remark="夹具旋转目标角度" />
|
||||
<Signal Name="MC_FixRot_Speed" Type="single" StartAddr="86" IndexOrLength="" Remark="夹具旋转运动速度" />
|
||||
<Signal Name="MC_FixRot_JogPos" Type="byte" StartAddr="90" IndexOrLength="" Remark="夹具旋转正向Jog,0:缺省,1:正向点动" />
|
||||
<Signal Name="MC_FixRot_JogNeg" Type="byte" StartAddr="91" IndexOrLength="" Remark="夹具旋转反向Jog,0:缺省,1:反向点动" />
|
||||
<Signal Name="MC_FixRot_Home" Type="byte" StartAddr="92" IndexOrLength="" Remark="夹具旋转回零,0:缺省,1:触发回零" />
|
||||
<Signal Name="MC_FixRot_Stop" Type="byte" StartAddr="93" IndexOrLength="" Remark="夹具旋转停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_Door_Open" Type="byte" StartAddr="94" IndexOrLength="" Remark="安全门开门,0:缺省,1:触发开门" />
|
||||
<Signal Name="MC_Door_Close" Type="byte" StartAddr="95" IndexOrLength="" Remark="安全门关门,0:缺省,1:触发关门" />
|
||||
<Signal Name="MC_Door_Stop" Type="byte" StartAddr="96" IndexOrLength="" Remark="安全门停止,0:缺省,1:触发停止" />
|
||||
<Signal Name="MC_VirtualJoystick_Enable" Type="bool" StartAddr="111" IndexOrLength="" Remark="虚拟摇杆使能"/>
|
||||
<Signal Name="MC_SourceDetZ_Linkage_Enable" Type="bool" StartAddr="101" IndexOrLength="" Remark="联动使能"/>
|
||||
</Group>
|
||||
<Group ID="ReadCommon" DBNumber="31">
|
||||
<Signal Name="ProbeA" Type="single" StartAddr="228" IndexOrLength="" Remark="测座角度A" />
|
||||
<Signal Name="ProbeB" Type="string" StartAddr="225" IndexOrLength="20" Remark="测座角度B" />
|
||||
<Signal Name="test" Type="byte" StartAddr="222" IndexOrLength="" Remark="" />
|
||||
<Signal Name="PlcLive" Type="byte" StartAddr="200" IndexOrLength="" Remark="PLC心跳,01周期变化表示PLC存活" />
|
||||
<Signal Name="PlcAlarm" Type="byte" StartAddr="201" IndexOrLength="" Remark="系统报警,0:缺省,10:有报警" />
|
||||
<Signal Name="MC_SourceZ_Pos" Type="single" StartAddr="100" IndexOrLength="" Remark="射线源Z实际位置" />
|
||||
<Signal Name="MC_DetZ_Pos" Type="single" StartAddr="104" IndexOrLength="" Remark="探测器Z实际位置" />
|
||||
<Signal Name="MC_StageX_Pos" Type="single" StartAddr="108" IndexOrLength="" Remark="载物台X实际位置" />
|
||||
<Signal Name="MC_StageY_Pos" Type="single" StartAddr="112" IndexOrLength="" Remark="载物台Y实际位置" />
|
||||
<Signal Name="MC_DetSwing_Angle" Type="single" StartAddr="116" IndexOrLength="" Remark="探测器摆动实际角度" />
|
||||
<Signal Name="MC_StageRot_Angle" Type="single" StartAddr="120" IndexOrLength="" Remark="载物台旋转实际角度" />
|
||||
<Signal Name="MC_FixRot_Angle" Type="single" StartAddr="124" IndexOrLength="" Remark="夹具旋转实际角度" />
|
||||
<Signal Name="MC_Door_Status" Type="byte" StartAddr="128" IndexOrLength="" Remark="安全门状态,0:未知,1:正在开门,2:已开,3:正在关门,4:已关,5:已锁定,6:故障" />
|
||||
<Signal Name="MC_Door_Interlock" Type="byte" StartAddr="130" IndexOrLength="" Remark="安全门联锁信号,0:缺省(无联锁),10:联锁有效(禁止开门)" />
|
||||
<Signal Name="MC_Joystick_Active" Type="bool" StartAddr="110" IndexOrLength="" Remark="实体摇杆输入"/>
|
||||
</Group>
|
||||
<Group ID="Status" DBNumber="4100">
|
||||
<Signal Name="ScanMode" Type="byte" StartAddr="201" IndexOrLength="" Remark="扫描模式,0:缺省(空闲),具体值由PLC定义" />
|
||||
</Group>
|
||||
</Config>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"document": {
|
||||
"pageSize": "A4",
|
||||
"orientation": "Portrait",
|
||||
"margins": { "top": 40, "bottom": 20, "left": 20, "right": 20 },
|
||||
"header": {
|
||||
"enabled": true,
|
||||
"left": [
|
||||
"${loc:Report_Title}",
|
||||
"${loc:Report_Id}:${metadata.reportId} | ${loc:Report_Date}:${formatDate(metadata.inspectionDate)} | ${loc:Report_Sample}:${metadata.sampleName}"
|
||||
],
|
||||
"rightImageKey": "companyLogo",
|
||||
"fontSize": 7,
|
||||
"color": "#666666",
|
||||
"showLine": true
|
||||
},
|
||||
"footer": {
|
||||
"enabled": true,
|
||||
"left": ["${CompanyName}"],
|
||||
"right": ["{currentPage} / {totalPages}"],
|
||||
"fontSize": 8,
|
||||
"color": "#666666",
|
||||
"showLine": true
|
||||
}
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"type": "homepage",
|
||||
"elements": [
|
||||
{
|
||||
"type": "row", "size": [170, 40], "positioning": "flow",
|
||||
"children": [
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "companyLogo", "size": [50, 30], "align": "left" },
|
||||
{ "type": "text", "content": "${CompanyName}", "style": "companyName", "align": "left" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "column", "align": "right",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "softwareLogo", "size": [15, 20], "align": "right" },
|
||||
{ "type": "text", "content": "${SoftwareName}", "style": "companyName", "align": "right" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "spacer", "size": [170, 15], "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Title}", "style": "title", "positioning": "flow" },
|
||||
{ "type": "spacer", "size": [170, 10], "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Id}:${metadata.reportId}", "style": "homepageInfo", "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Sample}:${metadata.sampleName}", "style": "homepageInfo", "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Description}:${metadata.description}", "style": "homepageInfo", "positioning": "flow" },
|
||||
{ "type": "image", "dataKey": "workpieceImage", "size": [160, 110], "border": true, "align": "center", "style": "imageDefault", "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Operator}:${metadata.operatorName}", "style": "homepageFooter", "positioning": "flow" },
|
||||
{ "type": "text", "content": "${loc:Report_Date}:${formatDate(metadata.inspectionDate)}", "style": "homepageFooter", "positioning": "flow" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "summary",
|
||||
"elements": [
|
||||
{ "type": "text", "content": "${loc:Report_Summary}", "style": "homepageHeading", "positioning": "flow" },
|
||||
{
|
||||
"type": "table", "dataKey": "summaryTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
|
||||
"columns": [
|
||||
{ "header": "${loc:Field_InspectionType}", "field": "inspectionType", "width": 50, "align": "left" },
|
||||
{ "header": "${loc:Field_Result}", "field": "classification", "width": 30, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } },
|
||||
{ "header": "${loc:Field_Status}", "field": "status", "width": 30, "align": "center", "colorRules": { "合格": "#008000", "PASS": "#008000", "不合格": "#FF0000", "FAIL": "#FF0000" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "metricData",
|
||||
"elements": [
|
||||
{ "type": "text", "content": "${loc:Page_MetricData}", "style": "heading", "positioning": "flow" },
|
||||
{
|
||||
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
|
||||
"children": [
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "lineMeasurementImage", "size": [110, 90], "border": true, "align": "left" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "text", "content": "${loc:Measurement_Type}:${measurementType}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Field_Point1}:${point1}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Field_Point2}:${point2}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_Distance}:${actualDistance} ${unit}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_Angle}:${angle}°", "style": "body", "align": "left" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "bgaInspection",
|
||||
"elements": [
|
||||
{ "type": "text", "content": "${loc:Page_BgaInspection}", "style": "heading", "positioning": "flow" },
|
||||
{
|
||||
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
|
||||
"children": [
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "bgaInspectionImage", "size": [110, 90], "border": true, "align": "left" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "text", "content": "${loc:Bga_Count}:${bgaCount}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_VoidRate}:${formatPercent(voidRate)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_FillRate}:${formatPercent(fillRate)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Bga_TotalArea}:${formatNumber(totalBgaArea, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Bga_TotalVoidArea}:${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Bga_VoidLimit}:${formatPercent(voidLimit)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Table_Classification}:${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "table", "dataKey": "bgaBallsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
|
||||
"columns": [
|
||||
{ "header": "${loc:Bga_BallIndex}", "field": "index", "width": 25, "align": "center" },
|
||||
{ "header": "${loc:Table_VoidRate}", "field": "voidRate", "width": 40, "align": "center" },
|
||||
{ "header": "${loc:Table_Area}", "field": "area", "width": 40, "align": "center" },
|
||||
{ "header": "${loc:Table_Classification}", "field": "classification", "width": 35, "align": "center", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "voidInspection",
|
||||
"elements": [
|
||||
{ "type": "text", "content": "${loc:Page_VoidInspection}", "style": "heading", "positioning": "flow" },
|
||||
{
|
||||
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
|
||||
"children": [
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "voidInspectionImage", "size": [110, 90], "border": true, "align": "left" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "text", "content": "${loc:Void_RoiArea}:${formatNumber(roiArea, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Void_TotalArea}:${formatNumber(totalVoidArea, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_VoidRate}:${formatPercent(voidRate)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Void_Limit}:${formatPercent(voidLimit)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Void_Count}:${voidCount}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Void_MaxArea}:${formatNumber(maxVoidArea, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Table_Classification}:${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "table", "dataKey": "voidsTable", "positioning": "flow", "size": [170, 0], "style": "tableDefault",
|
||||
"columns": [
|
||||
{ "header": "${loc:Table_Index}", "field": "index", "width": 20, "align": "center" },
|
||||
{ "header": "${loc:Table_Area}", "field": "area", "width": 35, "align": "center" },
|
||||
{ "header": "${loc:Table_AreaPercent}", "field": "areaPercent", "width": 35, "align": "center" },
|
||||
{ "header": "${loc:Table_CenterX}", "field": "centerX", "width": 30, "align": "center" },
|
||||
{ "header": "${loc:Table_CenterY}", "field": "centerY", "width": 30, "align": "center" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "viaFillInspection",
|
||||
"elements": [
|
||||
{ "type": "text", "content": "${loc:Page_ViaFillInspection}", "style": "heading", "positioning": "flow" },
|
||||
{
|
||||
"type": "row", "size": [170, 100], "widths": [6, 4], "positioning": "flow",
|
||||
"children": [
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "image", "dataKey": "viaFillImage", "size": [110, 90], "border": true, "align": "left" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "column", "align": "left",
|
||||
"children": [
|
||||
{ "type": "text", "content": "${loc:Fill_Rate}:${formatPercent(fillRate)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Measurement_VoidRate}:${formatPercent(voidRate)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Fill_FullDistance}:${formatNumber(fullDistance, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Fill_FillDistance}:${formatNumber(fillDistance, 2)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Fill_THTLimit}:${formatPercent(thtLimit)}", "style": "body", "align": "left" },
|
||||
{ "type": "text", "content": "${loc:Table_Classification}:${classification}", "style": "conclusion", "align": "left", "colorRules": { "Pass": "#008000", "Fail": "#FF0000" } }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"styles": {
|
||||
"title": { "font": "auto", "size": 28, "bold": true, "italic": false, "color": "#1a1a1a", "align": "center" },
|
||||
"companyName": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" },
|
||||
"homepageInfo": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "left", "paddingLeft": 10 },
|
||||
"homepageFooter": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center" },
|
||||
"homepageHeading": { "font": "auto", "size": 14, "bold": true, "italic": false, "color": "#333333", "align": "left" },
|
||||
"heading": { "font": "auto", "size": 16, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginBottom": 3 },
|
||||
"body": { "font": "auto", "size": 11, "bold": false, "italic": false, "color": "#333333", "align": "left" },
|
||||
"bodyBold": { "font": "auto", "size": 11, "bold": true, "italic": false, "color": "#1a1a1a", "align": "left" },
|
||||
"conclusion": { "font": "auto", "size": 12, "bold": true, "italic": false, "color": "#333333", "align": "left", "marginTop": 3 },
|
||||
"imageDefault": { "font": "auto", "size": 12, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5, "marginBottom": 5 },
|
||||
"tableHeader": { "font": "auto", "size": 10, "bold": true, "italic": false, "color": "#ffffff", "align": "center", "backgroundColor": "#4472C4" },
|
||||
"tableDefault": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center", "marginTop": 5 },
|
||||
"tableCell": { "font": "auto", "size": 10, "bold": false, "italic": false, "color": "#333333", "align": "center" }
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
// Flag, ob die Trace-Meldungen asynchron zu protokolliern sind: 1 für asynchron, 0 für synchron.
|
||||
1
|
||||
// Kategorie Konfigurationen: <Kategorie-Name> <Enabled[0/1]>
|
||||
FXDriver 0
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"Language": "zh-CN",
|
||||
"LogLevel": "Debug"
|
||||
"LogLevel": "Debug",
|
||||
"CameraType": "Hikvision"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -108,3 +108,5 @@ dotnet build XplorePlane.sln -c Release
|
||||
- [x] 主界面硬件栏相机设置按钮
|
||||
- [x] 打通与硬件层的调用流程
|
||||
- [x] 打通与图像层的调用流程
|
||||
- [ ] CNC的执行、存储逻辑的开发测试
|
||||
- [ ] 涉及到图像校准,矩阵
|
||||
|
||||
+11
-4
@@ -11,6 +11,11 @@
|
||||
<add key="UserManual" value="UserManual.pdf" />
|
||||
<add key="DeviceId" value="PlanerCT001" />
|
||||
|
||||
<!-- 授权配置 | License configuration -->
|
||||
<add key="License:LicenseMode" value="0" />
|
||||
<add key="License:ModuleId" value="4" />
|
||||
<add key="License:UseSma" value="false" />
|
||||
<add key="License:LicenseState" value="20" />
|
||||
|
||||
<!-- Serilog日志配置 -->
|
||||
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
|
||||
@@ -66,7 +71,6 @@
|
||||
<add key="Detector:Port" value="5000" />
|
||||
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
|
||||
<add key="Detector:AutoSave" value="true" />
|
||||
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
|
||||
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
|
||||
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
|
||||
<!-- 增益模式: Low, High | Gain mode: Low, High -->
|
||||
@@ -78,14 +82,13 @@
|
||||
<add key="Detector:Varex:ROI_Y" value="0" />
|
||||
<add key="Detector:Varex:ROI_Width" value="2880" />
|
||||
<add key="Detector:Varex:ROI_Height" value="2880" />
|
||||
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
|
||||
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
|
||||
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
|
||||
<!-- 默认增益值 | Default gain value -->
|
||||
<add key="Detector:IRay:DefaultGain" value="1.0" />
|
||||
<!-- 校正配置 | Correction Configuration -->
|
||||
<add key="Detector:Correction:DarkFrameCount" value="10" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="10" />
|
||||
<add key="Detector:Correction:DarkFrameCount" value="64" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="64" />
|
||||
<add key="Detector:Correction:SaveCorrectionData" value="true" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
@@ -163,6 +166,10 @@
|
||||
<!-- 运行参数 | Runtime parameters -->
|
||||
<add key="MotionControl:PollingInterval" value="500" />
|
||||
<add key="MotionControl:DefaultVelocity" value="500" />
|
||||
<!-- 射线源与探测器Z轴联动配置 | Source-Detector Z-axis linkage configuration -->
|
||||
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold" value="1.0" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent" value="100" />
|
||||
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1018,6 +1018,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"SixLabors.ImageSharp/3.1.12": {
|
||||
"runtime": {
|
||||
"lib/net6.0/SixLabors.ImageSharp.dll": {
|
||||
"assemblyVersion": "3.0.0.0",
|
||||
"fileVersion": "3.1.12.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
|
||||
"dependencies": {
|
||||
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
|
||||
@@ -2256,6 +2264,7 @@
|
||||
"Serilog.Settings.Configuration": "10.0.0",
|
||||
"Serilog.Sinks.Console": "6.1.1",
|
||||
"Serilog.Sinks.File": "7.0.0",
|
||||
"SixLabors.ImageSharp": "3.1.12",
|
||||
"Telerik.UI.for.Wpf.NetCore.Xaml": "2024.1.408"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -2422,6 +2431,7 @@
|
||||
"Serilog": "4.3.1",
|
||||
"Serilog.Sinks.Console": "6.1.1",
|
||||
"Serilog.Sinks.File": "7.0.0",
|
||||
"XP.Common": "1.0.0",
|
||||
"XP.ImageProcessing.Core": "1.0.0"
|
||||
},
|
||||
"runtime": {
|
||||
@@ -3311,6 +3321,13 @@
|
||||
"path": "sharpdx.mathematics/4.2.0",
|
||||
"hashPath": "sharpdx.mathematics.4.2.0.nupkg.sha512"
|
||||
},
|
||||
"SixLabors.ImageSharp/3.1.12": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
"sha512": "sha512-iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A==",
|
||||
"path": "sixlabors.imagesharp/3.1.12",
|
||||
"hashPath": "sixlabors.imagesharp.3.1.12.nupkg.sha512"
|
||||
},
|
||||
"SQLitePCLRaw.bundle_e_sqlite3/2.1.11": {
|
||||
"type": "package",
|
||||
"serviceable": true,
|
||||
|
||||
Binary file not shown.
@@ -5,127 +5,88 @@
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<appSettings>
|
||||
<!-- 语言配置 可选值: ZhCN, ZhTW, EnUS| Language Configuration -->
|
||||
<add key="Language" value="ZhCN" />
|
||||
<add key="XpData:RootPath" value="D:\XPData" />
|
||||
<add key="UserManual" value="UserManual.pdf" />
|
||||
<add key="DeviceId" value="PlanerCT001" />
|
||||
|
||||
|
||||
<!-- Serilog日志配置 -->
|
||||
<add key="License:LicenseMode" value="0" />
|
||||
<add key="License:ModuleId" value="4" />
|
||||
<add key="License:UseSma" value="false" />
|
||||
<add key="License:LicenseState" value="10" />
|
||||
<add key="Serilog:LogPath" value="D:\XplorePlane\Logs" />
|
||||
<add key="Serilog:MinimumLevel" value="Debug" />
|
||||
<add key="Serilog:EnableConsole" value="true" />
|
||||
<add key="Serilog:RollingInterval" value="Day" />
|
||||
<add key="Serilog:FileSizeLimitMB" value="100" />
|
||||
<add key="Serilog:RetainedFileCountLimit" value="365" />
|
||||
|
||||
<!-- 数据库SQLite配置 -->
|
||||
<add key="Sqlite:DbFilePath" value="D:\XplorePlane\DataBase\XP.db" />
|
||||
<add key="Sqlite:ConnectionTimeout" value="10" />
|
||||
<add key="Sqlite:CreateIfNotExists" value="true" />
|
||||
<!-- 是否启用SQLite WAL模式(提升并发读写性能,默认true) -->
|
||||
<add key="Sqlite:EnableWalMode" value="true" />
|
||||
<!-- 是否开启SQL操作日志(记录所有执行的SQL语句,默认false) -->
|
||||
<add key="Sqlite:EnableSqlLogging" value="false" />
|
||||
|
||||
<!-- 射线源配置 -->
|
||||
<!-- 射线源类型 | Ray Source Type -->
|
||||
<!-- 可选值: Comet225 | Available values: Comet225 -->
|
||||
<add key="RaySource:SourceType" value="Comet225" />
|
||||
<add key="RaySource:SerialNumber" value="SN08602861" />
|
||||
<add key="RaySource:TotalLifeThreshold" value="10" />
|
||||
<!-- PVI通讯参数 | PVI Communication Parameters -->
|
||||
<add key="RaySource:PlcIpAddress" value="192.168.12.10" />
|
||||
<add key="RaySource:PlcPort" value="11159" />
|
||||
<add key="RaySource:PortNumber" value="11" />
|
||||
<add key="RaySource:StationNumber" value="1" />
|
||||
<add key="RaySource:CpuName" value="cpu" />
|
||||
<add key="RaySource:ConnectionTimeout" value="30000" />
|
||||
<!-- 硬件参数范围 | Hardware Parameter Ranges -->
|
||||
<add key="RaySource:MinVoltage" value="20" />
|
||||
<add key="RaySource:MaxVoltage" value="225" />
|
||||
<add key="RaySource:MinCurrent" value="10" />
|
||||
<add key="RaySource:MaxCurrent" value="1440" />
|
||||
<!-- 外部程序配置 | External Program Configuration -->
|
||||
<add key="RaySource:AdvanceExePath" value="C:\Program Files (x86)\Feinfocus\FXEControl_3.1.1.65\FXEControl.exe" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="RaySource:RaySource:InitializationTimeout" value="30000" />
|
||||
<add key="RaySource:WarmUpTimeout" value="600000" />
|
||||
<add key="RaySource:StartUpTimeout" value="1800000" />
|
||||
<add key="RaySource:AutoCenterTimeout" value="1200000" />
|
||||
<add key="RaySource:FilamentAdjustTimeout" value="1200000" />
|
||||
<add key="RaySource:GeneralOperationTimeout" value="100000" />
|
||||
|
||||
<!-- 探测器配置 -->
|
||||
<!-- 探测器类型 | Detector Type -->
|
||||
<!-- 可选值: Varex, IRay, Hamamatsu | Available values: Varex, IRay, Hamamatsu -->
|
||||
<add key="Detector:Type" value="Varex" />
|
||||
<!-- 通用配置 | Common Configuration -->
|
||||
<add key="Detector:IP" value="192.168.1.200" />
|
||||
<add key="Detector:Port" value="5000" />
|
||||
<add key="Detector:SavePath" value="D:\XplorePlane\DetectorImages" />
|
||||
<add key="Detector:AutoSave" value="true" />
|
||||
<!-- Varex 探测器专属配置 | Varex Detector Specific Configuration -->
|
||||
<!-- Binning 模式: Bin1x1, Bin2x2, Bin4x4 | Binning mode: Bin1x1, Bin2x2, Bin4x4 -->
|
||||
<add key="Detector:Varex:BinningMode" value="Bin1x1" />
|
||||
<!-- 增益模式: Low, High | Gain mode: Low, High -->
|
||||
<add key="Detector:Varex:GainMode" value="High" />
|
||||
<!-- 曝光时间(毫秒)| Exposure time (milliseconds) -->
|
||||
<add key="Detector:Varex:ExposureTime" value="100" />
|
||||
<!-- ROI 区域 | ROI Region -->
|
||||
<add key="Detector:Varex:ROI_X" value="0" />
|
||||
<add key="Detector:Varex:ROI_Y" value="0" />
|
||||
<add key="Detector:Varex:ROI_Width" value="2880" />
|
||||
<add key="Detector:Varex:ROI_Height" value="2880" />
|
||||
<!-- iRay 探测器专属配置 | iRay Detector Specific Configuration -->
|
||||
<!-- 采集模式: Continuous, SingleFrame | Acquisition mode: Continuous, SingleFrame -->
|
||||
<add key="Detector:IRay:AcquisitionMode" value="Continuous" />
|
||||
<!-- 默认增益值 | Default gain value -->
|
||||
<add key="Detector:IRay:DefaultGain" value="1.0" />
|
||||
<!-- 校正配置 | Correction Configuration -->
|
||||
<add key="Detector:Correction:DarkFrameCount" value="10" />
|
||||
<add key="Detector:Correction:GainFrameCount" value="10" />
|
||||
<add key="Detector:Correction:SaveCorrectionData" value="true" />
|
||||
<!-- 操作超时配置 | Operation Timeout Configuration -->
|
||||
<add key="Detector:InitializationTimeout" value="30000" />
|
||||
<add key="Detector:AcquisitionTimeout" value="10000" />
|
||||
<add key="Detector:CorrectionTimeout" value="60000" />
|
||||
<!-- 主界面实时图像与探测器帧流水线 -->
|
||||
<add key="MainViewport:RealtimeEnabledDefault" value="true" />
|
||||
<add key="DetectorPipeline:AcquireQueueCapacity" value="16" />
|
||||
<add key="DetectorPipeline:ProcessQueueCapacity" value="8" />
|
||||
<add key="DetectorPipeline:ProcessEveryNFrames" value="1" />
|
||||
|
||||
<!-- Dump 配置 | Dump Configuration -->
|
||||
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||
<add key="Dump:EnableScheduledDump" value="false" />
|
||||
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
|
||||
<add key="Dump:RetentionDays" value="7" />
|
||||
|
||||
<!-- PLC 配置 | PLC Configuration -->
|
||||
<add key="Plc:IpAddress" value="192.168.0.1" />
|
||||
<add key="Plc:Port" value="102" />
|
||||
<add key="Plc:Rack" value="0" />
|
||||
<add key="Plc:Slot" value="1" />
|
||||
<!-- PlcType可选值: S200Smart, S300, S400, S1200, S1500 | PlcType Available values: S200Smart, S300, S400, S1200, S1500 -->
|
||||
<add key="Plc:PlcType" value="S1200" />
|
||||
<!-- 数据块配置 | Data Block Configuration -->
|
||||
<add key="Plc:ReadDbBlock" value="DB31" />
|
||||
<add key="Plc:WriteDbBlock" value="DB31" />
|
||||
<add key="Plc:ReadStartAddress" value="0" />
|
||||
<add key="Plc:ReadLength" value="200" />
|
||||
<!-- 批量读取周期(毫秒)| Bulk read interval (ms) -->
|
||||
<add key="Plc:BulkReadIntervalMs" value="250" />
|
||||
<!-- 超时配置 | Timeout Configuration -->
|
||||
<add key="Plc:ConnectTimeoutMs" value="3000" />
|
||||
<add key="Plc:ReadTimeoutMs" value="1000" />
|
||||
<add key="Plc:WriteTimeoutMs" value="1000" />
|
||||
<!-- 自动重连 | Auto Reconnection -->
|
||||
<add key="Plc:bReConnect" value="true" />
|
||||
|
||||
<!-- 直线轴配置(单位:mm)| Linear axis config (unit: mm) -->
|
||||
<add key="MotionControl:SourceZ:Min" value="-500" />
|
||||
<add key="MotionControl:SourceZ:Max" value="500" />
|
||||
<add key="MotionControl:SourceZ:Origin" value="0" />
|
||||
@@ -138,7 +99,6 @@
|
||||
<add key="MotionControl:StageY:Min" value="-150" />
|
||||
<add key="MotionControl:StageY:Max" value="150" />
|
||||
<add key="MotionControl:StageY:Origin" value="0" />
|
||||
<!-- 旋转轴配置(单位:度)| Rotary axis config (unit: degrees) -->
|
||||
<add key="MotionControl:DetectorSwing:Min" value="-45" />
|
||||
<add key="MotionControl:DetectorSwing:Max" value="45" />
|
||||
<add key="MotionControl:DetectorSwing:Origin" value="0" />
|
||||
@@ -151,46 +111,33 @@
|
||||
<add key="MotionControl:FixtureRotation:Max" value="90" />
|
||||
<add key="MotionControl:FixtureRotation:Origin" value="0" />
|
||||
<add key="MotionControl:FixtureRotation:Enabled" value="false" />
|
||||
<!-- 几何原点(mm)| Geometry origins (mm) -->
|
||||
<add key="MotionControl:Geometry:SourceZOrigin" value="0" />
|
||||
<add key="MotionControl:Geometry:DetectorZOrigin" value="600" />
|
||||
<add key="MotionControl:Geometry:StageRotationCenterZ" value="300" />
|
||||
<!-- 探测器摆动几何参数(mm)| Detector swing geometry parameters (mm) -->
|
||||
<!-- SwingPivotOffset: Pivot 相对于 DetectorZ 绝对坐标的 Z 方向偏移,正值表示 Pivot 在 DetectorZ_abs 上方 -->
|
||||
<add key="MotionControl:Geometry:SwingPivotOffset" value="80" />
|
||||
<!-- SwingRadius: Pivot 到探测器感光面中心的距离,为 0 时退化为无摆动模型 -->
|
||||
<add key="MotionControl:Geometry:SwingRadius" value="200" />
|
||||
<!-- 运行参数 | Runtime parameters -->
|
||||
<add key="MotionControl:PollingInterval" value="500" />
|
||||
<add key="MotionControl:DefaultVelocity" value="500" />
|
||||
|
||||
<!-- 报告输出文件夹路径(绝对路径)| Report output directory (absolute path) -->
|
||||
<add key="MotionControl:SourceDetectorZLinkage:Enabled" value="true" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:TriggerThreshold"
|
||||
value="1.0" />
|
||||
<add key="MotionControl:SourceDetectorZLinkage:SpeedPercent"
|
||||
value="100" />
|
||||
<add key="Report:OutputDirectory" value="D:\XplorePlane\Report" />
|
||||
<!-- 报告模板文件路径(相对于应用程序目录或绝对路径)| Template file path (relative to app dir or absolute) -->
|
||||
<add key="Report:TemplatePath" value="Templates\StandardReportTemplate.json" />
|
||||
<!-- 输出文件名模式 | File name pattern -->
|
||||
<add key="Report:FileNamePattern" value="{Date}_{ProductName}_{WorkpieceSN}_{ReportId}" />
|
||||
<!-- 文件名重复时是否自动累加序号 | Auto-increment suffix when file name duplicates-->
|
||||
<add key="Report:AutoIncrementOnDuplicate" value="true" />
|
||||
<!-- 生成后是否自动打开 PDF 阅读器 | Auto-open PDF viewer after generation (true/false) -->
|
||||
<add key="Report:AutoOpenAfterGenerate" value="false" />
|
||||
<!-- 默认页面尺寸 | Default page size (A4) -->
|
||||
<add key="Report:DefaultPageSize" value="A4" />
|
||||
<!-- 默认页面方向 | Default orientation (Portrait / Landscape) -->
|
||||
<add key="Report:DefaultOrientation" value="Portrait" />
|
||||
<!-- 页面边距(mm),有效范围 0-100 | Page margins (mm), valid range 0-100 -->
|
||||
<add key="Report:MarginTop" value="20" />
|
||||
<add key="Report:MarginBottom" value="20" />
|
||||
<add key="Report:MarginLeft" value="20" />
|
||||
<add key="Report:MarginRight" value="20" />
|
||||
<!-- 报告中显示的公司名称 | Company name displayed in report -->
|
||||
<add key="Report:CompanyName" value="海克斯康制造智能技术(青岛)有限公司" />
|
||||
<!-- 公司 Logo 图片路径(可选,为空则不显示)| Company logo path (optional, empty = no logo) -->
|
||||
<add key="Report:CompanyLogo" value="Templates\Logo.png" />
|
||||
<!-- 软件信息配置 | Software Information Configuration -->
|
||||
<add key="Report:SoftwareName" value="XplorePlane" />
|
||||
<add key="Report:SoftwareLogo" value="Templates\Logo2.png" />
|
||||
|
||||
</appSettings>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -5,6 +5,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="850"
|
||||
d:DesignWidth="1400">
|
||||
@@ -88,6 +89,20 @@
|
||||
<Image Source="/XP.Camera;component/Calibration/Resources/外部导入.png" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="采集当前点"
|
||||
Command="{Binding CapturePointCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<Image Source="/XP.Camera;component/Calibration/Resources/执行.png" Width="24" Height="24" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="删除选中"
|
||||
Command="{Binding DeleteSelectedPointCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
<Button.Tag>
|
||||
<TextBlock Text="✕" FontSize="20" Foreground="Red" HorizontalAlignment="Center" />
|
||||
</Button.Tag>
|
||||
</Button>
|
||||
<Button Content="{Binding Source={StaticResource LocalizedStrings}, Path=CalibrationExecute}"
|
||||
Command="{Binding CalibrateCommand}" FontFamily="Segoe UI"
|
||||
Style="{StaticResource ToolbarButtonStyle}">
|
||||
@@ -113,6 +128,32 @@
|
||||
VerticalAlignment="Center" FontFamily="Segoe UI"
|
||||
IsChecked="{Binding ShowWorldCoordinates}"
|
||||
Margin="10,0,0,0" FontSize="13" Foreground="{StaticResource TextColor}" />
|
||||
<ToggleButton IsChecked="{Binding IsLiveView}" VerticalAlignment="Center" Margin="10,0,0,0"
|
||||
Width="80" Height="66" Cursor="Hand" FontFamily="Segoe UI">
|
||||
<ToggleButton.Template>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border x:Name="Bd" Background="White" BorderBrush="#E1E1E1"
|
||||
BorderThickness="1" CornerRadius="6">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="Icon" Text="▶" FontSize="20" HorizontalAlignment="Center" Margin="0,2,0,3" />
|
||||
<TextBlock x:Name="Label" Text="实时" FontSize="10.5" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Icon" Property="Text" Value="⏸" />
|
||||
<Setter TargetName="Label" Property="Text" Value="冻结" />
|
||||
<Setter TargetName="Bd" Property="Background" Value="#E8F5E9" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#81C784" />
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background" Value="#EAF2FB" />
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="#B0D4F1" />
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</ToggleButton.Template>
|
||||
</ToggleButton>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -136,6 +177,7 @@
|
||||
|
||||
<DataGrid Grid.Row="1" AutoGenerateColumns="False" CanUserAddRows="True"
|
||||
ItemsSource="{Binding CalibrationPoints}"
|
||||
SelectedItem="{Binding SelectedPoint}"
|
||||
HeadersVisibility="Column" GridLinesVisibility="All"
|
||||
FontFamily="Segoe UI"
|
||||
BorderBrush="{StaticResource BorderColor}" BorderThickness="1">
|
||||
@@ -159,7 +201,8 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,12,8" />
|
||||
<roi:PolygonRoiCanvas Grid.Row="0" x:Name="roiCanvas" Margin="12,12,12,8"
|
||||
ImageSource="{Binding ImageSource}" />
|
||||
|
||||
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||
Margin="12,0,12,12" Padding="12" MinHeight="80">
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
using System.Drawing;
|
||||
using System.ComponentModel;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using XP.Camera.Calibration.ViewModels;
|
||||
using WpfBrushes = System.Windows.Media.Brushes;
|
||||
using WpfColor = System.Windows.Media.Color;
|
||||
using XP.ImageProcessing.RoiControl.Controls;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
@@ -23,82 +22,42 @@ public partial class CalibrationControl : UserControl
|
||||
if (DataContext is CalibrationViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
|
||||
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||
imageCanvas.RoiCanvas.Children.Clear();
|
||||
};
|
||||
|
||||
imageCanvas.CanvasRightMouseUp += ImageCanvas_RightMouseUp;
|
||||
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (_viewModel?.CurrentImage == null) return;
|
||||
|
||||
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||
imageCanvas.ZoomScale *= zoom;
|
||||
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||
}
|
||||
|
||||
private void ImageCanvas_RightMouseUp(object? sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (_viewModel?.CurrentImage == null) return;
|
||||
|
||||
var pos = e.GetPosition(imageCanvas.RoiCanvas);
|
||||
float imageX = (float)pos.X;
|
||||
float imageY = (float)pos.Y;
|
||||
|
||||
if (imageX >= 0 && imageX < _viewModel.CurrentImage.Width &&
|
||||
imageY >= 0 && imageY < _viewModel.CurrentImage.Height)
|
||||
if (e.PropertyName == nameof(CalibrationViewModel.OverlayImage))
|
||||
{
|
||||
var pixelPoint = new PointF(imageX, imageY);
|
||||
var worldPoint = _viewModel.ConvertPixelToWorld(pixelPoint);
|
||||
|
||||
_viewModel.StatusText = $"像素坐标: ({imageX:F2}, {imageY:F2})\n世界坐标: ({worldPoint.X:F2}, {worldPoint.Y:F2})";
|
||||
|
||||
DrawMarkerOnCanvas(imageX, imageY, worldPoint);
|
||||
UpdateDetectionOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMarkerOnCanvas(float imageX, float imageY, PointF worldPoint)
|
||||
private void UpdateDetectionOverlay()
|
||||
{
|
||||
imageCanvas.RoiCanvas.Children.Clear();
|
||||
|
||||
var ellipse = new System.Windows.Shapes.Ellipse
|
||||
if (_viewModel?.OverlayImage == null)
|
||||
{
|
||||
Width = 10, Height = 10,
|
||||
Stroke = WpfBrushes.Red, StrokeThickness = 2,
|
||||
Fill = WpfBrushes.Transparent
|
||||
roiCanvas.ClearDetectionOverlay();
|
||||
return;
|
||||
}
|
||||
|
||||
var overlayCanvas = new Canvas
|
||||
{
|
||||
Width = _viewModel.OverlayImage.PixelWidth,
|
||||
Height = _viewModel.OverlayImage.PixelHeight,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(ellipse, imageX - 5);
|
||||
Canvas.SetTop(ellipse, imageY - 5);
|
||||
imageCanvas.RoiCanvas.Children.Add(ellipse);
|
||||
|
||||
var pixelText = new TextBlock
|
||||
var image = new System.Windows.Controls.Image
|
||||
{
|
||||
Text = $"P:({imageX:F0},{imageY:F0})",
|
||||
Foreground = WpfBrushes.Red, FontSize = 12,
|
||||
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||
Source = _viewModel.OverlayImage,
|
||||
Width = _viewModel.OverlayImage.PixelWidth,
|
||||
Height = _viewModel.OverlayImage.PixelHeight,
|
||||
IsHitTestVisible = false
|
||||
};
|
||||
Canvas.SetLeft(pixelText, imageX + 10);
|
||||
Canvas.SetTop(pixelText, imageY - 20);
|
||||
imageCanvas.RoiCanvas.Children.Add(pixelText);
|
||||
|
||||
if (_viewModel?.ShowWorldCoordinates == true)
|
||||
{
|
||||
var worldText = new TextBlock
|
||||
{
|
||||
Text = $"W:({worldPoint.X:F2},{worldPoint.Y:F2})",
|
||||
Foreground = WpfBrushes.Blue, FontSize = 12,
|
||||
Background = new System.Windows.Media.SolidColorBrush(WpfColor.FromArgb(180, 255, 255, 255))
|
||||
};
|
||||
Canvas.SetLeft(worldText, imageX + 10);
|
||||
Canvas.SetTop(worldText, imageY + 5);
|
||||
imageCanvas.RoiCanvas.Children.Add(worldText);
|
||||
}
|
||||
overlayCanvas.Children.Add(image);
|
||||
roiCanvas.SetDetectionOverlayCanvas(overlayCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:cal="clr-namespace:XP.Camera.Calibration"
|
||||
xmlns:controls="clr-namespace:XP.Camera.Calibration.Controls"
|
||||
xmlns:roi="clr-namespace:XP.ImageProcessing.RoiControl.Controls;assembly=XP.ImageProcessing.RoiControl"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="900"
|
||||
d:DesignWidth="1600">
|
||||
@@ -168,7 +169,8 @@
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<controls:ImageCanvasControl Grid.Row="0" x:Name="imageCanvas" Margin="12,12,8,8" />
|
||||
<roi:PolygonRoiCanvas Grid.Row="0" x:Name="roiCanvas" Margin="12,12,8,8"
|
||||
ImageSource="{Binding ImageSource}" />
|
||||
<Border Grid.Row="1" Background="White" BorderBrush="{StaticResource BorderColor}" BorderThickness="1"
|
||||
Margin="12,0,8,12" Padding="12" Height="70">
|
||||
<Grid>
|
||||
|
||||
@@ -1,46 +1,13 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using XP.Camera.Calibration.ViewModels;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
public partial class ChessboardCalibrationControl : UserControl
|
||||
{
|
||||
private ChessboardCalibrationViewModel? _viewModel;
|
||||
|
||||
public ChessboardCalibrationControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += ChessboardCalibrationControl_Loaded;
|
||||
}
|
||||
|
||||
private void ChessboardCalibrationControl_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is ChessboardCalibrationViewModel viewModel)
|
||||
{
|
||||
_viewModel = viewModel;
|
||||
|
||||
_viewModel.ImageLoadedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = _viewModel.ImageSource;
|
||||
};
|
||||
|
||||
_viewModel.ImageClearedRequested += (s, e) =>
|
||||
{
|
||||
imageCanvas.ReferenceImage = null;
|
||||
};
|
||||
|
||||
imageCanvas.CanvasMouseWheel += ImageCanvas_MouseWheel;
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageCanvas_MouseWheel(object? sender, MouseWheelEventArgs e)
|
||||
{
|
||||
if (_viewModel?.ImageSource == null) return;
|
||||
|
||||
double zoom = e.Delta > 0 ? 1.1 : 0.9;
|
||||
imageCanvas.ZoomScale *= zoom;
|
||||
imageCanvas.ZoomScale = Math.Max(0.1, Math.Min(imageCanvas.ZoomScale, 10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
<UserControl x:Class="XP.Camera.Calibration.Controls.ImageCanvasControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="600" d:DesignWidth="800" x:Name="imageCanvasControl">
|
||||
<Border ClipToBounds="True" RenderOptions.BitmapScalingMode="NearestNeighbor">
|
||||
<Viewbox>
|
||||
<AdornerDecorator x:Name="adorner" MouseWheel="Adorner_MouseWheel">
|
||||
<AdornerDecorator.RenderTransform>
|
||||
<TransformGroup>
|
||||
<TranslateTransform X="{Binding PanningOffsetX, ElementName=imageCanvasControl}"
|
||||
Y="{Binding PanningOffsetY, ElementName=imageCanvasControl}" />
|
||||
<ScaleTransform ScaleX="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||
ScaleY="{Binding ZoomScale, ElementName=imageCanvasControl}"
|
||||
CenterX="{Binding ZoomCenter.X, ElementName=imageCanvasControl}"
|
||||
CenterY="{Binding ZoomCenter.Y, ElementName=imageCanvasControl}" />
|
||||
</TransformGroup>
|
||||
</AdornerDecorator.RenderTransform>
|
||||
<Grid PreviewMouseMove="Canvas_MouseMove"
|
||||
PreviewMouseLeftButtonUp="Canvas_MouseLeftButtonUp"
|
||||
PreviewMouseRightButtonUp="Canvas_MouseRightButtonUp"
|
||||
MouseEnter="Canvas_MouseEnter"
|
||||
PreviewMouseLeftButtonDown="Canvas_MouseLeftButtonDown"
|
||||
PreviewMouseRightButtonDown="Canvas_MouseRightButtonDown">
|
||||
<ContentPresenter Content="{Binding RoiCanvas, ElementName=imageCanvasControl}"
|
||||
SizeChanged="ContentPresenter_SizeChanged" />
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</Viewbox>
|
||||
</Border>
|
||||
</UserControl>
|
||||
@@ -1,229 +0,0 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XP.Camera.Calibration.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// 图像画布控件 - 提供图像显示、缩放、平移功能
|
||||
/// </summary>
|
||||
public partial class ImageCanvasControl : UserControl
|
||||
{
|
||||
private Point mouseDownPoint = new Point();
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty ZoomScaleProperty =
|
||||
DependencyProperty.Register("ZoomScale", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||
|
||||
public static readonly DependencyProperty ZoomCenterProperty =
|
||||
DependencyProperty.Register("ZoomCenter", typeof(Point), typeof(ImageCanvasControl), new PropertyMetadata(new Point()));
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetXProperty =
|
||||
DependencyProperty.Register("PanningOffsetX", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||
|
||||
public static readonly DependencyProperty PanningOffsetYProperty =
|
||||
DependencyProperty.Register("PanningOffsetY", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(0.0));
|
||||
|
||||
public static readonly DependencyProperty ReferenceImageProperty =
|
||||
DependencyProperty.Register("ReferenceImage", typeof(BitmapSource), typeof(ImageCanvasControl),
|
||||
new UIPropertyMetadata(null, ReferenceImageChanged));
|
||||
|
||||
public static readonly DependencyProperty ImageScaleFactorProperty =
|
||||
DependencyProperty.Register("ImageScaleFactor", typeof(double), typeof(ImageCanvasControl), new PropertyMetadata(1.0));
|
||||
|
||||
public static readonly DependencyProperty MaxImageWidthProperty =
|
||||
DependencyProperty.Register("MaxImageWidth", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty MaxImageHeightProperty =
|
||||
DependencyProperty.Register("MaxImageHeight", typeof(int), typeof(ImageCanvasControl), new PropertyMetadata(0));
|
||||
|
||||
public static readonly DependencyProperty EnablePanningProperty =
|
||||
DependencyProperty.Register("EnablePanning", typeof(bool), typeof(ImageCanvasControl), new PropertyMetadata(true));
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public double ZoomScale
|
||||
{
|
||||
get => (double)GetValue(ZoomScaleProperty);
|
||||
set => SetValue(ZoomScaleProperty, value);
|
||||
}
|
||||
|
||||
public Point ZoomCenter
|
||||
{
|
||||
get => (Point)GetValue(ZoomCenterProperty);
|
||||
set => SetValue(ZoomCenterProperty, value);
|
||||
}
|
||||
|
||||
public double PanningOffsetX
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetXProperty);
|
||||
set => SetValue(PanningOffsetXProperty, value);
|
||||
}
|
||||
|
||||
public double PanningOffsetY
|
||||
{
|
||||
get => (double)GetValue(PanningOffsetYProperty);
|
||||
set => SetValue(PanningOffsetYProperty, value);
|
||||
}
|
||||
|
||||
public BitmapSource? ReferenceImage
|
||||
{
|
||||
get => (BitmapSource?)GetValue(ReferenceImageProperty);
|
||||
set => SetValue(ReferenceImageProperty, value);
|
||||
}
|
||||
|
||||
public double ImageScaleFactor
|
||||
{
|
||||
get => (double)GetValue(ImageScaleFactorProperty);
|
||||
set => SetValue(ImageScaleFactorProperty, value);
|
||||
}
|
||||
|
||||
public int MaxImageWidth
|
||||
{
|
||||
get => (int)GetValue(MaxImageWidthProperty);
|
||||
set => SetValue(MaxImageWidthProperty, value);
|
||||
}
|
||||
|
||||
public int MaxImageHeight
|
||||
{
|
||||
get => (int)GetValue(MaxImageHeightProperty);
|
||||
set => SetValue(MaxImageHeightProperty, value);
|
||||
}
|
||||
|
||||
public bool EnablePanning
|
||||
{
|
||||
get => (bool)GetValue(EnablePanningProperty);
|
||||
set => SetValue(EnablePanningProperty, value);
|
||||
}
|
||||
|
||||
private Canvas roiCanvas = new Canvas();
|
||||
public Canvas RoiCanvas
|
||||
{
|
||||
get => roiCanvas;
|
||||
set => roiCanvas = value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseUp;
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasRightMouseDown;
|
||||
public event EventHandler<MouseButtonEventArgs>? CanvasLeftMouseDown;
|
||||
public event EventHandler<MouseEventArgs>? CanvasMouseMove;
|
||||
public event EventHandler<MouseWheelEventArgs>? CanvasMouseWheel;
|
||||
|
||||
#endregion
|
||||
|
||||
public ImageCanvasControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private static void ReferenceImageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
(d as ImageCanvasControl)?.OnReferenceImageChanged(e.NewValue as BitmapSource);
|
||||
}
|
||||
|
||||
private void OnReferenceImageChanged(BitmapSource? bitmapSource)
|
||||
{
|
||||
if (bitmapSource != null)
|
||||
{
|
||||
ImageBrush brush = new ImageBrush { ImageSource = bitmapSource, Stretch = Stretch.Uniform };
|
||||
RoiCanvas.Background = brush;
|
||||
RoiCanvas.Height = bitmapSource.Height;
|
||||
RoiCanvas.Width = bitmapSource.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
RoiCanvas.Height = MaxImageHeight > 0 ? MaxImageHeight : 600;
|
||||
RoiCanvas.Width = MaxImageWidth > 0 ? MaxImageWidth : 800;
|
||||
RoiCanvas.Background = Brushes.LightGray;
|
||||
}
|
||||
}
|
||||
|
||||
private double CalculateScaleFactor()
|
||||
{
|
||||
if (ActualWidth <= 0) return 1;
|
||||
double scaleFactor = Math.Max(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||
if (scaleFactor < 0)
|
||||
scaleFactor = Math.Min(RoiCanvas.Width / ActualWidth, RoiCanvas.Height / ActualHeight);
|
||||
return scaleFactor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void Canvas_MouseEnter(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton == MouseButtonState.Pressed)
|
||||
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||
}
|
||||
|
||||
private void Canvas_MouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
CanvasMouseMove?.Invoke(sender, e);
|
||||
if (EnablePanning && e.LeftButton == MouseButtonState.Pressed)
|
||||
{
|
||||
Point mousePoint = e.GetPosition(RoiCanvas);
|
||||
double mouseMoveLength = Point.Subtract(mousePoint, mouseDownPoint).Length;
|
||||
if (mouseMoveLength > (10 * CalculateScaleFactor()) / ZoomScale)
|
||||
{
|
||||
PanningOffsetX += mousePoint.X - mouseDownPoint.X;
|
||||
PanningOffsetY += mousePoint.Y - mouseDownPoint.Y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { }
|
||||
|
||||
private void Canvas_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CanvasRightMouseUp?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void Canvas_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
mouseDownPoint = e.GetPosition(RoiCanvas);
|
||||
CanvasLeftMouseDown?.Invoke(sender, e);
|
||||
if (EnablePanning && e.ClickCount == 2)
|
||||
{
|
||||
ResetView();
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void Canvas_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
CanvasRightMouseDown?.Invoke(sender, e);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Adorner_MouseWheel(object sender, MouseWheelEventArgs e)
|
||||
{
|
||||
CanvasMouseWheel?.Invoke(sender, e);
|
||||
}
|
||||
|
||||
private void ContentPresenter_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ImageScaleFactor = CalculateScaleFactor();
|
||||
}
|
||||
|
||||
private void ResetView()
|
||||
{
|
||||
ZoomScale = 1.0;
|
||||
PanningOffsetX = 0.0;
|
||||
PanningOffsetY = 0.0;
|
||||
ZoomCenter = new Point(0, 0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Drawing;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace XP.Camera.Calibration;
|
||||
|
||||
/// <summary>
|
||||
/// 标定采集服务接口
|
||||
/// 提供"一键采集"能力:读取编码器坐标 + 拍图 + 识别标记中心
|
||||
/// </summary>
|
||||
public interface ICalibrationCaptureService
|
||||
{
|
||||
/// <summary>是否可用(相机已连接、运动系统就绪)</summary>
|
||||
bool IsAvailable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 采集当前标定点
|
||||
/// </summary>
|
||||
/// <returns>采集结果,失败时返回 null</returns>
|
||||
CaptureResult? CaptureCurrentPoint();
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前导航相机图像
|
||||
/// </summary>
|
||||
BitmapSource? CaptureImage();
|
||||
|
||||
/// <summary>
|
||||
/// 启动实时预览(将相机实时画面推送到 LiveImageUpdated 事件)
|
||||
/// </summary>
|
||||
void StartLivePreview();
|
||||
|
||||
/// <summary>
|
||||
/// 停止实时预览
|
||||
/// </summary>
|
||||
void StopLivePreview();
|
||||
|
||||
/// <summary>
|
||||
/// 实时画面更新事件
|
||||
/// </summary>
|
||||
event EventHandler<LiveImageEventArgs>? LiveImageUpdated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 实时画面事件参数
|
||||
/// </summary>
|
||||
public class LiveImageEventArgs : EventArgs
|
||||
{
|
||||
public BitmapSource Image { get; }
|
||||
public LiveImageEventArgs(BitmapSource image) => Image = image;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单次采集结果
|
||||
/// </summary>
|
||||
public class CaptureResult
|
||||
{
|
||||
/// <summary>标记中心像素坐标 X(亚像素)</summary>
|
||||
public double PixelX { get; set; }
|
||||
|
||||
/// <summary>标记中心像素坐标 Y(亚像素)</summary>
|
||||
public double PixelY { get; set; }
|
||||
|
||||
/// <summary>平台编码器坐标 X (mm)</summary>
|
||||
public double WorldX { get; set; }
|
||||
|
||||
/// <summary>平台编码器坐标 Y (mm)</summary>
|
||||
public double WorldY { get; set; }
|
||||
|
||||
/// <summary>采集的图像</summary>
|
||||
public BitmapSource? Image { get; set; }
|
||||
|
||||
/// <summary>检测到的标记轮廓点集</summary>
|
||||
public System.Drawing.Point[]? ContourPoints { get; set; }
|
||||
}
|
||||
@@ -11,16 +11,20 @@ namespace XP.Camera.Calibration.ViewModels;
|
||||
public class CalibrationViewModel : BindableBase
|
||||
{
|
||||
private readonly ICalibrationDialogService _dialogService;
|
||||
private readonly ICalibrationCaptureService? _captureService;
|
||||
private readonly CalibrationProcessor _calibrator = new();
|
||||
private Image<Bgr, byte>? _currentImage;
|
||||
private static readonly ILogger _logger = Log.ForContext<CalibrationViewModel>();
|
||||
private BitmapSource? _imageSource;
|
||||
private BitmapSource? _frozenImage;
|
||||
private string _statusText = Res.CalibrationStatusReady;
|
||||
private bool _showWorldCoordinates;
|
||||
private bool _isLiveView = true;
|
||||
|
||||
public CalibrationViewModel(ICalibrationDialogService dialogService)
|
||||
public CalibrationViewModel(ICalibrationDialogService dialogService, ICalibrationCaptureService? captureService = null)
|
||||
{
|
||||
_dialogService = dialogService;
|
||||
_captureService = captureService;
|
||||
CalibrationPoints = new ObservableCollection<CalibrationProcessor.CalibrationPoint>();
|
||||
|
||||
LoadImageCommand = new DelegateCommand(LoadImage);
|
||||
@@ -29,6 +33,16 @@ public class CalibrationViewModel : BindableBase
|
||||
.ObservesProperty(() => CalibrationPoints.Count);
|
||||
SaveCalibrationCommand = new DelegateCommand(SaveCalibration);
|
||||
LoadCalibrationCommand = new DelegateCommand(LoadCalibration);
|
||||
CapturePointCommand = new DelegateCommand(CapturePoint, CanCapturePoint);
|
||||
DeleteSelectedPointCommand = new DelegateCommand(DeleteSelectedPoint, () => SelectedPoint != null)
|
||||
.ObservesProperty(() => SelectedPoint);
|
||||
|
||||
// 启动实时预览
|
||||
if (_captureService != null)
|
||||
{
|
||||
_captureService.LiveImageUpdated += OnLiveImageUpdated;
|
||||
_captureService.StartLivePreview();
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableCollection<CalibrationProcessor.CalibrationPoint> CalibrationPoints { get; }
|
||||
@@ -39,6 +53,14 @@ public class CalibrationViewModel : BindableBase
|
||||
set => SetProperty(ref _imageSource, value);
|
||||
}
|
||||
|
||||
private BitmapSource? _overlayImage;
|
||||
/// <summary>叠加层图像(显示检测到的轮廓和中心点)</summary>
|
||||
public BitmapSource? OverlayImage
|
||||
{
|
||||
get => _overlayImage;
|
||||
set => SetProperty(ref _overlayImage, value);
|
||||
}
|
||||
|
||||
public string StatusText
|
||||
{
|
||||
get => _statusText;
|
||||
@@ -51,11 +73,48 @@ public class CalibrationViewModel : BindableBase
|
||||
set => SetProperty(ref _showWorldCoordinates, value);
|
||||
}
|
||||
|
||||
public bool IsLiveView
|
||||
{
|
||||
get => _isLiveView;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _isLiveView, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(LiveViewButtonText));
|
||||
if (value)
|
||||
{
|
||||
// 切回实时:恢复实时预览
|
||||
_captureService?.StartLivePreview();
|
||||
}
|
||||
else
|
||||
{
|
||||
// 切到当前:冻结当前帧
|
||||
_frozenImage = _imageSource;
|
||||
_captureService?.StopLivePreview();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string LiveViewButtonText => _isLiveView ? "⏸ 冻结" : "▶ 实时";
|
||||
|
||||
public DelegateCommand LoadImageCommand { get; }
|
||||
public DelegateCommand LoadCsvCommand { get; }
|
||||
public DelegateCommand CalibrateCommand { get; }
|
||||
public DelegateCommand SaveCalibrationCommand { get; }
|
||||
public DelegateCommand LoadCalibrationCommand { get; }
|
||||
public DelegateCommand CapturePointCommand { get; }
|
||||
public DelegateCommand DeleteSelectedPointCommand { get; }
|
||||
|
||||
/// <summary>是否支持采集模式(有采集服务注入)</summary>
|
||||
public bool IsCaptureAvailable => _captureService?.IsAvailable == true;
|
||||
|
||||
private CalibrationProcessor.CalibrationPoint? _selectedPoint;
|
||||
public CalibrationProcessor.CalibrationPoint? SelectedPoint
|
||||
{
|
||||
get => _selectedPoint;
|
||||
set => SetProperty(ref _selectedPoint, value);
|
||||
}
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
@@ -130,6 +189,59 @@ public class CalibrationViewModel : BindableBase
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanCapturePoint() => _captureService?.IsAvailable == true;
|
||||
|
||||
private void CapturePoint()
|
||||
{
|
||||
if (_captureService == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var result = _captureService.CaptureCurrentPoint();
|
||||
if (result == null)
|
||||
{
|
||||
StatusText = "采集失败:未能识别标记点,请确认标记在视野内";
|
||||
return;
|
||||
}
|
||||
|
||||
CalibrationPoints.Add(new CalibrationProcessor.CalibrationPoint
|
||||
{
|
||||
PixelX = result.PixelX,
|
||||
PixelY = result.PixelY,
|
||||
WorldX = result.WorldX,
|
||||
WorldY = result.WorldY
|
||||
});
|
||||
|
||||
// 更新图像显示
|
||||
if (result.Image != null)
|
||||
{
|
||||
ImageSource = result.Image;
|
||||
}
|
||||
|
||||
// 绘制检测结果叠加层(轮廓 + 中心点)
|
||||
DrawDetectionOverlay(result);
|
||||
|
||||
StatusText = $"已采集第 {CalibrationPoints.Count} 个点: 像素({result.PixelX:F1}, {result.PixelY:F1}) → 物理({result.WorldX:F3}, {result.WorldY:F3})";
|
||||
_logger.Information("标定点采集: Pixel=({PixelX:F1}, {PixelY:F1}), World=({WorldX:F3}, {WorldY:F3})",
|
||||
result.PixelX, result.PixelY, result.WorldX, result.WorldY);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"采集异常: {ex.Message}";
|
||||
_logger.Error(ex, "标定点采集失败");
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteSelectedPoint()
|
||||
{
|
||||
if (SelectedPoint != null && CalibrationPoints.Contains(SelectedPoint))
|
||||
{
|
||||
CalibrationPoints.Remove(SelectedPoint);
|
||||
SelectedPoint = null;
|
||||
StatusText = $"已删除,剩余 {CalibrationPoints.Count} 个标定点";
|
||||
}
|
||||
}
|
||||
|
||||
public PointF ConvertPixelToWorld(PointF pixel) => _calibrator.PixelToWorld(pixel);
|
||||
|
||||
public Image<Bgr, byte>? CurrentImage => _currentImage;
|
||||
@@ -138,6 +250,82 @@ public class CalibrationViewModel : BindableBase
|
||||
|
||||
private void RaiseEvent(EventHandler? handler) => handler?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
private void OnLiveImageUpdated(object? sender, LiveImageEventArgs e)
|
||||
{
|
||||
if (!_isLiveView) return;
|
||||
System.Windows.Application.Current?.Dispatcher?.BeginInvoke(() =>
|
||||
{
|
||||
ImageSource = e.Image;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制检测结果叠加层(轮廓 + 中心十字 + 坐标文字)
|
||||
/// </summary>
|
||||
private void DrawDetectionOverlay(CaptureResult result)
|
||||
{
|
||||
if (result.Image == null) return;
|
||||
|
||||
int w = result.Image.PixelWidth;
|
||||
int h = result.Image.PixelHeight;
|
||||
|
||||
// 创建透明叠加层
|
||||
var visual = new System.Windows.Media.DrawingVisual();
|
||||
using (var dc = visual.RenderOpen())
|
||||
{
|
||||
// 绘制轮廓
|
||||
if (result.ContourPoints != null && result.ContourPoints.Length > 2)
|
||||
{
|
||||
var pen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Lime, 2);
|
||||
var geometry = new System.Windows.Media.StreamGeometry();
|
||||
using (var ctx = geometry.Open())
|
||||
{
|
||||
ctx.BeginFigure(new System.Windows.Point(result.ContourPoints[0].X, result.ContourPoints[0].Y), false, true);
|
||||
for (int i = 1; i < result.ContourPoints.Length; i++)
|
||||
ctx.LineTo(new System.Windows.Point(result.ContourPoints[i].X, result.ContourPoints[i].Y), true, false);
|
||||
}
|
||||
geometry.Freeze();
|
||||
dc.DrawGeometry(null, pen, geometry);
|
||||
}
|
||||
|
||||
// 绘制中心十字
|
||||
double cx = result.PixelX;
|
||||
double cy = result.PixelY;
|
||||
double crossSize = Math.Max(10, Math.Max(w, h) / 80.0);
|
||||
var crossPen = new System.Windows.Media.Pen(System.Windows.Media.Brushes.Red, 2);
|
||||
dc.DrawLine(crossPen, new System.Windows.Point(cx - crossSize, cy), new System.Windows.Point(cx + crossSize, cy));
|
||||
dc.DrawLine(crossPen, new System.Windows.Point(cx, cy - crossSize), new System.Windows.Point(cx, cy + crossSize));
|
||||
|
||||
// 绘制坐标文字
|
||||
var text = new System.Windows.Media.FormattedText(
|
||||
$"({result.PixelX:F1}, {result.PixelY:F1})",
|
||||
System.Globalization.CultureInfo.CurrentCulture,
|
||||
System.Windows.FlowDirection.LeftToRight,
|
||||
new System.Windows.Media.Typeface("Segoe UI"),
|
||||
Math.Max(12, Math.Max(w, h) / 60.0),
|
||||
System.Windows.Media.Brushes.Yellow,
|
||||
1.0);
|
||||
dc.DrawText(text, new System.Windows.Point(cx + crossSize + 4, cy - text.Height / 2));
|
||||
}
|
||||
|
||||
var rtb = new System.Windows.Media.Imaging.RenderTargetBitmap(w, h, 96, 96, System.Windows.Media.PixelFormats.Pbgra32);
|
||||
rtb.Render(visual);
|
||||
rtb.Freeze();
|
||||
OverlayImage = rtb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止实时预览并清理资源(窗口关闭时调用)
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
if (_captureService != null)
|
||||
{
|
||||
_captureService.StopLivePreview();
|
||||
_captureService.LiveImageUpdated -= OnLiveImageUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||
{
|
||||
using var bitmap = mat.ToBitmap();
|
||||
|
||||
@@ -10,6 +10,7 @@ public static class PixelConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将原始像素数据转换为 WPF 的 BitmapSource 对象。
|
||||
/// 支持 Mono8、BGR8、RGB8、BGRA8 以及 Bayer 8-bit 格式(自动解码为 BGR24)。
|
||||
/// 返回的 BitmapSource 已调用 Freeze(),可跨线程访问。
|
||||
/// </summary>
|
||||
public static BitmapSource ToBitmapSource(byte[] pixelData, int width, int height, string pixelFormat)
|
||||
@@ -19,11 +20,23 @@ public static class PixelConverter
|
||||
if (height <= 0) throw new ArgumentException("Height must be a positive integer.", nameof(height));
|
||||
ArgumentNullException.ThrowIfNull(pixelFormat);
|
||||
|
||||
var (format, stride) = pixelFormat switch
|
||||
string normalized = NormalizePixelFormat(pixelFormat);
|
||||
|
||||
// Bayer 格式需要解码
|
||||
if (normalized.StartsWith("Bayer"))
|
||||
{
|
||||
byte[] bgrData = DemosaicBayer(pixelData, width, height, normalized);
|
||||
var bmp = BitmapSource.Create(width, height, 96, 96, PixelFormats.Bgr24, null, bgrData, width * 3);
|
||||
bmp.Freeze();
|
||||
return bmp;
|
||||
}
|
||||
|
||||
var (format, stride) = normalized switch
|
||||
{
|
||||
"Mono8" => (PixelFormats.Gray8, width),
|
||||
"BGR8" => (PixelFormats.Bgr24, width * 3),
|
||||
"BGRA8" => (PixelFormats.Bgra32, width * 4),
|
||||
"RGB8" => (PixelFormats.Rgb24, width * 3),
|
||||
_ => throw new NotSupportedException($"Pixel format '{pixelFormat}' is not supported.")
|
||||
};
|
||||
|
||||
@@ -31,4 +44,136 @@ public static class PixelConverter
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将不同 SDK 的像素格式名称统一为标准名称。
|
||||
/// </summary>
|
||||
private static string NormalizePixelFormat(string pixelFormat)
|
||||
{
|
||||
if (pixelFormat is "Mono8" or "BGR8" or "BGRA8" or "RGB8")
|
||||
return pixelFormat;
|
||||
|
||||
var upper = pixelFormat.ToUpperInvariant();
|
||||
|
||||
if (upper.Contains("MONO8")) return "Mono8";
|
||||
if (upper.Contains("BGR8")) return "BGR8";
|
||||
if (upper.Contains("BGRA8")) return "BGRA8";
|
||||
if (upper.Contains("RGB8") && !upper.Contains("BAYER")) return "RGB8";
|
||||
|
||||
// Bayer 格式
|
||||
if (upper.Contains("BAYERRG8") || upper.Contains("BAYER_RG8")) return "BayerRG8";
|
||||
if (upper.Contains("BAYERGR8") || upper.Contains("BAYER_GR8")) return "BayerGR8";
|
||||
if (upper.Contains("BAYERGB8") || upper.Contains("BAYER_GB8")) return "BayerGB8";
|
||||
if (upper.Contains("BAYERBG8") || upper.Contains("BAYER_BG8")) return "BayerBG8";
|
||||
|
||||
return pixelFormat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 简单 Bayer 解码(双线性插值),输出 BGR24。
|
||||
/// </summary>
|
||||
private static byte[] DemosaicBayer(byte[] bayer, int width, int height, string pattern)
|
||||
{
|
||||
// pattern: BayerRG8, BayerGR8, BayerGB8, BayerBG8
|
||||
// RG: R G GR: G R GB: G B BG: B G
|
||||
// G B B G R G G R
|
||||
|
||||
int rRow, rCol; // 红色像素在2x2块中的位置
|
||||
switch (pattern)
|
||||
{
|
||||
case "BayerRG8": rRow = 0; rCol = 0; break;
|
||||
case "BayerGR8": rRow = 0; rCol = 1; break;
|
||||
case "BayerGB8": rRow = 1; rCol = 0; break;
|
||||
case "BayerBG8": rRow = 1; rCol = 1; break;
|
||||
default: rRow = 0; rCol = 0; break;
|
||||
}
|
||||
|
||||
byte[] bgr = new byte[width * height * 3];
|
||||
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
int srcIdx = y * width + x;
|
||||
int dstIdx = (y * width + x) * 3;
|
||||
|
||||
// 确定当前像素在 Bayer 模式中的角色
|
||||
int py = (y + rRow) % 2; // 0=红行, 1=蓝行
|
||||
int px = (x + rCol) % 2; // 0=红列/蓝列, 1=绿列
|
||||
|
||||
byte r, g, b;
|
||||
|
||||
if (py == 0 && px == 0)
|
||||
{
|
||||
// 红色像素位置
|
||||
r = bayer[srcIdx];
|
||||
g = AvgNeighbors4(bayer, width, height, x, y);
|
||||
b = AvgDiagonal(bayer, width, height, x, y);
|
||||
}
|
||||
else if (py == 1 && px == 1)
|
||||
{
|
||||
// 蓝色像素位置
|
||||
b = bayer[srcIdx];
|
||||
g = AvgNeighbors4(bayer, width, height, x, y);
|
||||
r = AvgDiagonal(bayer, width, height, x, y);
|
||||
}
|
||||
else if (py == 0 && px == 1)
|
||||
{
|
||||
// 绿色像素(红行)
|
||||
g = bayer[srcIdx];
|
||||
r = AvgHorizontal(bayer, width, x, y);
|
||||
b = AvgVertical(bayer, width, height, x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 绿色像素(蓝行)
|
||||
g = bayer[srcIdx];
|
||||
b = AvgHorizontal(bayer, width, x, y);
|
||||
r = AvgVertical(bayer, width, height, x, y);
|
||||
}
|
||||
|
||||
bgr[dstIdx] = b;
|
||||
bgr[dstIdx + 1] = g;
|
||||
bgr[dstIdx + 2] = r;
|
||||
}
|
||||
}
|
||||
|
||||
return bgr;
|
||||
}
|
||||
|
||||
private static byte AvgNeighbors4(byte[] data, int w, int h, int x, int y)
|
||||
{
|
||||
int sum = 0, count = 0;
|
||||
if (x > 0) { sum += data[y * w + x - 1]; count++; }
|
||||
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
|
||||
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
|
||||
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
|
||||
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||
}
|
||||
|
||||
private static byte AvgDiagonal(byte[] data, int w, int h, int x, int y)
|
||||
{
|
||||
int sum = 0, count = 0;
|
||||
if (x > 0 && y > 0) { sum += data[(y - 1) * w + x - 1]; count++; }
|
||||
if (x < w - 1 && y > 0) { sum += data[(y - 1) * w + x + 1]; count++; }
|
||||
if (x > 0 && y < h - 1) { sum += data[(y + 1) * w + x - 1]; count++; }
|
||||
if (x < w - 1 && y < h - 1) { sum += data[(y + 1) * w + x + 1]; count++; }
|
||||
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||
}
|
||||
|
||||
private static byte AvgHorizontal(byte[] data, int w, int x, int y)
|
||||
{
|
||||
int sum = 0, count = 0;
|
||||
if (x > 0) { sum += data[y * w + x - 1]; count++; }
|
||||
if (x < w - 1) { sum += data[y * w + x + 1]; count++; }
|
||||
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||
}
|
||||
|
||||
private static byte AvgVertical(byte[] data, int w, int h, int x, int y)
|
||||
{
|
||||
int sum = 0, count = 0;
|
||||
if (y > 0) { sum += data[(y - 1) * w + x]; count++; }
|
||||
if (y < h - 1) { sum += data[(y + 1) * w + x]; count++; }
|
||||
return count > 0 ? (byte)(sum / count) : (byte)0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ public class CameraFactory : ICameraFactory
|
||||
return cameraType switch
|
||||
{
|
||||
"Basler" => new BaslerCameraController(),
|
||||
// "Hikvision" => new HikvisionCameraController(),
|
||||
_ => throw new NotSupportedException($"不支持的相机品牌: {cameraType}")
|
||||
"Hikvision" => new HikvisionCameraController(),
|
||||
_ => throw new NotSupportedException($"Unsupported Camera Type: {cameraType}")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
using MvCameraControl;
|
||||
using Serilog;
|
||||
|
||||
namespace XP.Camera;
|
||||
|
||||
/// <summary>
|
||||
/// 海康威视相机控制器,封装 MvCameraControl.Net SDK 实现 <see cref="ICameraController"/>。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>所有公共方法通过内部 <c>_syncLock</c> 对象进行 lock 同步,保证线程安全。</para>
|
||||
/// <para>事件回调(ImageGrabbed、GrabError)在 SDK 回调线程上触发,不持有 _syncLock,避免死锁。</para>
|
||||
/// </remarks>
|
||||
public class HikvisionCameraController : ICameraController
|
||||
{
|
||||
private static readonly ILogger _logger = Log.ForContext<HikvisionCameraController>();
|
||||
private static bool _sdkInitialized;
|
||||
private static readonly object _sdkInitLock = new();
|
||||
|
||||
private readonly object _syncLock = new();
|
||||
private IDevice? _device;
|
||||
private CameraInfo? _cachedCameraInfo;
|
||||
private bool _isConnected;
|
||||
private bool _isGrabbing;
|
||||
|
||||
public HikvisionCameraController()
|
||||
{
|
||||
// SDK 初始化延迟到 Open() 中执行
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsConnected
|
||||
{
|
||||
get { lock (_syncLock) { return _isConnected; } }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsGrabbing
|
||||
{
|
||||
get { lock (_syncLock) { return _isGrabbing; } }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler<GrabErrorEventArgs>? GrabError;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event EventHandler? ConnectionLost;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CameraInfo Open()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (_isConnected && _cachedCameraInfo != null)
|
||||
{
|
||||
_logger.Information("Hikvision camera already connected, returning cached info.");
|
||||
return _cachedCameraInfo;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Information("Opening Hikvision camera connection...");
|
||||
|
||||
// 确保 SDK 初始化
|
||||
EnsureSdkInitialized();
|
||||
|
||||
// 枚举设备
|
||||
DeviceTLayerType layerType = DeviceTLayerType.MvGigEDevice
|
||||
| DeviceTLayerType.MvUsbDevice;
|
||||
|
||||
List<IDeviceInfo> deviceInfoList;
|
||||
int ret = DeviceEnumerator.EnumDevices(layerType, out deviceInfoList);
|
||||
_logger.Information("EnumDevices(GigE|USB) returned: 0x{RetCode:X8}, device count: {Count}",
|
||||
ret, deviceInfoList?.Count ?? 0);
|
||||
|
||||
// 如果没找到,分别尝试
|
||||
if (ret == MvError.MV_OK && (deviceInfoList == null || deviceInfoList.Count == 0))
|
||||
{
|
||||
// 单独尝试 GigE
|
||||
List<IDeviceInfo> gigeList;
|
||||
int retGige = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvGigEDevice, out gigeList);
|
||||
_logger.Information("EnumDevices(GigE only) returned: 0x{RetCode:X8}, count: {Count}",
|
||||
retGige, gigeList?.Count ?? 0);
|
||||
|
||||
// 单独尝试 USB
|
||||
List<IDeviceInfo> usbList;
|
||||
int retUsb = DeviceEnumerator.EnumDevices(DeviceTLayerType.MvUsbDevice, out usbList);
|
||||
_logger.Information("EnumDevices(USB only) returned: 0x{RetCode:X8}, count: {Count}",
|
||||
retUsb, usbList?.Count ?? 0);
|
||||
|
||||
// 合并结果
|
||||
deviceInfoList = new List<IDeviceInfo>();
|
||||
if (gigeList != null) deviceInfoList.AddRange(gigeList);
|
||||
if (usbList != null) deviceInfoList.AddRange(usbList);
|
||||
}
|
||||
|
||||
if (ret != MvError.MV_OK)
|
||||
{
|
||||
throw new CameraException($"Enumerate Hikvision devices failed: 0x{ret:X8}");
|
||||
}
|
||||
|
||||
if (deviceInfoList == null || deviceInfoList.Count == 0)
|
||||
{
|
||||
throw new DeviceNotFoundException("No Hikvision camera device found.");
|
||||
}
|
||||
|
||||
// 选择第一个设备
|
||||
IDeviceInfo deviceInfo = deviceInfoList[0];
|
||||
_logger.Information("Found Hikvision device: {Model} (SN: {Serial})",
|
||||
deviceInfo.ModelName, deviceInfo.SerialNumber);
|
||||
|
||||
// 创建设备
|
||||
_device = DeviceFactory.CreateDevice(deviceInfo);
|
||||
|
||||
// 打开设备
|
||||
ret = _device.Open();
|
||||
if (ret != MvError.MV_OK)
|
||||
{
|
||||
_device.Dispose();
|
||||
_device = null;
|
||||
throw new CameraException($"Open Hikvision device failed: 0x{ret:X8}");
|
||||
}
|
||||
|
||||
// GigE 设备优化包大小
|
||||
if (_device is IGigEDevice gigEDevice)
|
||||
{
|
||||
int packetSize;
|
||||
ret = gigEDevice.GetOptimalPacketSize(out packetSize);
|
||||
if (ret == MvError.MV_OK && packetSize > 0)
|
||||
{
|
||||
_device.Parameters.SetIntValue("GevSCPSPacketSize", packetSize);
|
||||
_logger.Debug("Set GigE packet size to {PacketSize}", packetSize);
|
||||
}
|
||||
}
|
||||
|
||||
// 配置软件触发模式
|
||||
_device.Parameters.SetEnumValueByString("TriggerMode", "On");
|
||||
_device.Parameters.SetEnumValueByString("TriggerSource", "Software");
|
||||
|
||||
// 彩色相机:尝试设置输出为 BGR8 以便直接显示
|
||||
// 如果相机不支持 BGR8(如只支持 Bayer),则保持默认
|
||||
int fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "BGR8Packed");
|
||||
if (fmtRet != MvError.MV_OK)
|
||||
{
|
||||
// 尝试 Mono8(黑白相机)
|
||||
fmtRet = _device.Parameters.SetEnumValueByString("PixelFormat", "Mono8");
|
||||
}
|
||||
_logger.Debug("Set PixelFormat result: 0x{Ret:X8}", fmtRet);
|
||||
|
||||
_cachedCameraInfo = new CameraInfo(
|
||||
ModelName: deviceInfo.ModelName ?? "",
|
||||
SerialNumber: deviceInfo.SerialNumber ?? "",
|
||||
VendorName: deviceInfo.ManufacturerName ?? "",
|
||||
DeviceType: deviceInfo.TLayerType.ToString()
|
||||
);
|
||||
|
||||
_isConnected = true;
|
||||
_logger.Information("Hikvision camera connected: {ModelName} (SN: {SerialNumber})",
|
||||
_cachedCameraInfo.ModelName, _cachedCameraInfo.SerialNumber);
|
||||
|
||||
return _cachedCameraInfo;
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_device?.Dispose();
|
||||
_device = null;
|
||||
_logger.Error(ex, "Failed to open Hikvision camera.");
|
||||
throw new CameraException("Failed to open Hikvision camera device.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Close()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isConnected)
|
||||
{
|
||||
_logger.Information("Hikvision camera not connected, Close() ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_isGrabbing)
|
||||
{
|
||||
StopGrabbingInternal();
|
||||
}
|
||||
|
||||
_logger.Information("Closing Hikvision camera connection...");
|
||||
_device?.Close();
|
||||
_device?.Dispose();
|
||||
_device = null;
|
||||
_isConnected = false;
|
||||
_cachedCameraInfo = null;
|
||||
_logger.Information("Hikvision camera connection closed.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_device = null;
|
||||
_isConnected = false;
|
||||
_isGrabbing = false;
|
||||
_cachedCameraInfo = null;
|
||||
_logger.Error(ex, "Error while closing Hikvision camera.");
|
||||
throw new CameraException("Failed to close Hikvision camera device.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StartGrabbing()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
|
||||
if (_isGrabbing)
|
||||
{
|
||||
_logger.Information("Already grabbing, StartGrabbing() ignored.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Information("Starting Hikvision grabbing with software trigger...");
|
||||
|
||||
// 设置缓存节点数
|
||||
_device!.StreamGrabber.SetImageNodeNum(5);
|
||||
|
||||
// 注册回调
|
||||
_device.StreamGrabber.FrameGrabedEvent += OnFrameGrabbed;
|
||||
|
||||
// 开始采集
|
||||
int ret = _device.StreamGrabber.StartGrabbing();
|
||||
if (ret != MvError.MV_OK)
|
||||
{
|
||||
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
|
||||
throw new CameraException($"Start grabbing failed: 0x{ret:X8}");
|
||||
}
|
||||
|
||||
_isGrabbing = true;
|
||||
_logger.Information("Hikvision grabbing started.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to start Hikvision grabbing.");
|
||||
throw new CameraException("Failed to start grabbing.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteSoftwareTrigger()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isGrabbing)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot execute software trigger: camera is not grabbing.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int ret = _device!.Parameters.SetCommandValue("TriggerSoftware");
|
||||
if (ret != MvError.MV_OK)
|
||||
{
|
||||
throw new CameraException($"Execute software trigger failed: 0x{ret:X8}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException and not InvalidOperationException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to execute software trigger.");
|
||||
throw new CameraException("Failed to execute software trigger.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void StopGrabbing()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
if (!_isGrabbing) return;
|
||||
StopGrabbingInternal();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetExposureTime()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
IFloatValue floatValue;
|
||||
int ret = _device!.Parameters.GetFloatValue("ExposureTime", out floatValue);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Get ExposureTime failed: 0x{ret:X8}");
|
||||
return floatValue.CurValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetExposureTime(double microseconds)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
// 关闭自动曝光
|
||||
_device!.Parameters.SetEnumValueByString("ExposureAuto", "Off");
|
||||
int ret = _device.Parameters.SetFloatValue("ExposureTime", (float)microseconds);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Set ExposureTime failed: 0x{ret:X8}");
|
||||
_logger.Information("Hikvision exposure time set to {Microseconds} µs.", microseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public double GetGain()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
IFloatValue floatValue;
|
||||
int ret = _device!.Parameters.GetFloatValue("Gain", out floatValue);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Get Gain failed: 0x{ret:X8}");
|
||||
return floatValue.CurValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetGain(double value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
_device!.Parameters.SetEnumValueByString("GainAuto", "Off");
|
||||
int ret = _device.Parameters.SetFloatValue("Gain", (float)value);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Set Gain failed: 0x{ret:X8}");
|
||||
_logger.Information("Hikvision gain set to {Value}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetWidth()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
IIntValue intValue;
|
||||
int ret = _device!.Parameters.GetIntValue("Width", out intValue);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Get Width failed: 0x{ret:X8}");
|
||||
return (int)intValue.CurValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetWidth(int value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
int ret = _device!.Parameters.SetIntValue("Width", value);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Set Width failed: 0x{ret:X8}");
|
||||
_logger.Information("Hikvision width set to {Value}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int GetHeight()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
IIntValue intValue;
|
||||
int ret = _device!.Parameters.GetIntValue("Height", out intValue);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Get Height failed: 0x{ret:X8}");
|
||||
return (int)intValue.CurValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetHeight(int value)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
int ret = _device!.Parameters.SetIntValue("Height", value);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Set Height failed: 0x{ret:X8}");
|
||||
_logger.Information("Hikvision height set to {Value}.", value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetPixelFormat()
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
IEnumValue enumValue;
|
||||
int ret = _device!.Parameters.GetEnumValue("PixelFormat", out enumValue);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Get PixelFormat failed: 0x{ret:X8}");
|
||||
return enumValue.CurEnumEntry.Symbolic;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetPixelFormat(string format)
|
||||
{
|
||||
lock (_syncLock)
|
||||
{
|
||||
EnsureConnected();
|
||||
int ret = _device!.Parameters.SetEnumValueByString("PixelFormat", format);
|
||||
if (ret != MvError.MV_OK)
|
||||
throw new CameraException($"Set PixelFormat failed: 0x{ret:X8}");
|
||||
_logger.Information("Hikvision pixel format set to {Format}.", format);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Close();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 私有方法
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// SDK 回调:图像采集完成
|
||||
/// </summary>
|
||||
private void OnFrameGrabbed(object? sender, FrameGrabbedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var frameOut = e.FrameOut;
|
||||
if (frameOut == null || frameOut.Image == null)
|
||||
{
|
||||
_logger.Warning("Hikvision OnFrameGrabbed: FrameOut or Image is null");
|
||||
GrabError?.Invoke(this, new GrabErrorEventArgs(-1, "FrameOut or Image is null."));
|
||||
return;
|
||||
}
|
||||
|
||||
var image = frameOut.Image;
|
||||
int width = (int)image.Width;
|
||||
int height = (int)image.Height;
|
||||
int imageSize = (int)image.ImageSize;
|
||||
string pixelFormat = image.PixelType.ToString();
|
||||
|
||||
// 提取像素数据
|
||||
byte[] pixelData = image.PixelData ?? Array.Empty<byte>();
|
||||
|
||||
_logger.Debug("Hikvision frame: {Width}x{Height}, format={Format}, dataLen={Len}",
|
||||
width, height, pixelFormat, pixelData.Length);
|
||||
|
||||
if (pixelData.Length == 0)
|
||||
{
|
||||
_logger.Warning("Hikvision OnFrameGrabbed: PixelData is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var args = new ImageGrabbedEventArgs(pixelData, width, height, pixelFormat);
|
||||
ImageGrabbed?.Invoke(this, args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Exception in Hikvision OnFrameGrabbed handler.");
|
||||
}
|
||||
}
|
||||
|
||||
private void StopGrabbingInternal()
|
||||
{
|
||||
if (!_isGrabbing) return;
|
||||
|
||||
try
|
||||
{
|
||||
_device?.StreamGrabber.StopGrabbing();
|
||||
if (_device != null)
|
||||
_device.StreamGrabber.FrameGrabedEvent -= OnFrameGrabbed;
|
||||
_isGrabbing = false;
|
||||
_logger.Information("Hikvision grabbing stopped.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_isGrabbing = false;
|
||||
_logger.Error(ex, "Error while stopping Hikvision grabbing.");
|
||||
throw new CameraException("Failed to stop grabbing.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (!_isConnected)
|
||||
throw new InvalidOperationException("Hikvision camera is not connected. Call Open() first.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保 SDK 全局初始化(只调用一次)
|
||||
/// </summary>
|
||||
private static void EnsureSdkInitialized()
|
||||
{
|
||||
if (_sdkInitialized) return;
|
||||
lock (_sdkInitLock)
|
||||
{
|
||||
if (_sdkInitialized) return;
|
||||
try
|
||||
{
|
||||
int ret = SDKSystem.Initialize();
|
||||
if (ret != MvError.MV_OK)
|
||||
{
|
||||
_logger.Error("Hikvision SDK Initialize failed: 0x{ErrorCode:X8}", ret);
|
||||
throw new CameraException($"Hikvision SDK Initialize failed: 0x{ret:X8}");
|
||||
}
|
||||
_sdkInitialized = true;
|
||||
_logger.Information("Hikvision SDK initialized successfully.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not CameraException)
|
||||
{
|
||||
_logger.Error(ex, "Failed to initialize Hikvision SDK.");
|
||||
throw new CameraException("Failed to initialize Hikvision SDK.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,26 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>XP.Camera</RootNamespace>
|
||||
<AssemblyName>XP.Camera</AssemblyName>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Basler.Pylon">
|
||||
<HintPath>..\ExternalLibraries\Basler.Pylon.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="MvCameraControl.Net">
|
||||
<HintPath>..\ExternalLibraries\MvCameraControl.Net.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
<CopyLocal>true</CopyLocal>
|
||||
</Reference>
|
||||
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Emgu.CV.Bitmap" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Prism.DryIoc" Version="9.0.537" />
|
||||
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<ProjectReference Include="..\XP.Common\XP.Common.csproj" />
|
||||
<ProjectReference Include="..\XP.ImageProcessing.RoiControl\XP.ImageProcessing.RoiControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Telerik.Windows.Controls;
|
||||
using Telerik.Windows.Controls.ChartView;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// RadChartView 渲染适配器 | RadChartView rendering adapter
|
||||
/// 负责将直方图频次数据渲染到 Telerik RadCartesianChart 控件
|
||||
/// </summary>
|
||||
internal sealed class ChartRenderer
|
||||
{
|
||||
private readonly RadCartesianChart _chart;
|
||||
private readonly ScatterAreaSeries _areaSeries;
|
||||
private readonly LinearAxis _xAxis;
|
||||
|
||||
/// <summary>
|
||||
/// 16 位数据聚合因子 | 16-bit data aggregation factor
|
||||
/// </summary>
|
||||
private const int AggregationFactor = 256;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,接收 RadCartesianChart 实例 | Constructor, receives RadCartesianChart instance
|
||||
/// </summary>
|
||||
/// <param name="chart">图表控件实例 | Chart control instance</param>
|
||||
/// <param name="areaSeries">面积图系列 | Area series</param>
|
||||
/// <param name="xAxis">X 轴 | X axis</param>
|
||||
public ChartRenderer(RadCartesianChart chart, ScatterAreaSeries areaSeries, LinearAxis xAxis)
|
||||
{
|
||||
_chart = chart ?? throw new ArgumentNullException(nameof(chart));
|
||||
_areaSeries = areaSeries ?? throw new ArgumentNullException(nameof(areaSeries));
|
||||
_xAxis = xAxis ?? throw new ArgumentNullException(nameof(xAxis));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新直方图数据 | Update histogram data
|
||||
/// </summary>
|
||||
/// <param name="histogram">频次数组(256 或 65536 长度)| Frequency array (256 or 65536 length)</param>
|
||||
/// <param name="isLogarithmic">是否使用对数 Y 轴 | Whether to use logarithmic Y axis</param>
|
||||
public void UpdateData(long[] histogram, bool isLogarithmic)
|
||||
{
|
||||
if (histogram == null || histogram.Length == 0)
|
||||
return;
|
||||
|
||||
// 确定是否需要聚合(16 位数据)| Determine if aggregation needed (16-bit data)
|
||||
long[] displayData;
|
||||
int xAxisMax;
|
||||
|
||||
if (histogram.Length == 65536)
|
||||
{
|
||||
// 16 位数据聚合为 256 个柱体 | Aggregate 16-bit data to 256 bars
|
||||
displayData = Aggregate16BitHistogram(histogram);
|
||||
xAxisMax = 65535;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 8 位数据直接显示 | Display 8-bit data directly
|
||||
displayData = histogram;
|
||||
xAxisMax = 255;
|
||||
}
|
||||
|
||||
// 设置 X 轴范围 | Set X axis range
|
||||
_xAxis.Minimum = 0;
|
||||
_xAxis.Maximum = xAxisMax;
|
||||
// 根据范围自动设置刻度间隔(保持 4-5 个刻度)| Auto set major step based on range (keep 4-5 ticks)
|
||||
_xAxis.MajorStep = xAxisMax <= 255 ? 64 : 16384;
|
||||
|
||||
// 构建数据点 | Build data points
|
||||
var dataPoints = new List<HistogramDataPoint>();
|
||||
|
||||
if (isLogarithmic)
|
||||
{
|
||||
// 对数模式:频次为 0 的不绘制 | Logarithmic mode: skip zero frequency
|
||||
for (int i = 0; i < displayData.Length; i++)
|
||||
{
|
||||
if (displayData[i] > 0)
|
||||
{
|
||||
double xValue = histogram.Length == 65536
|
||||
? i * AggregationFactor + AggregationFactor / 2.0
|
||||
: i;
|
||||
dataPoints.Add(new HistogramDataPoint
|
||||
{
|
||||
GrayLevel = xValue,
|
||||
Frequency = displayData[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 线性模式:所有灰度级别都绘制 | Linear mode: draw all gray levels
|
||||
for (int i = 0; i < displayData.Length; i++)
|
||||
{
|
||||
double xValue = histogram.Length == 65536
|
||||
? i * AggregationFactor + AggregationFactor / 2.0
|
||||
: i;
|
||||
dataPoints.Add(new HistogramDataPoint
|
||||
{
|
||||
GrayLevel = xValue,
|
||||
Frequency = displayData[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新图表数据 | Update chart data
|
||||
_areaSeries.ItemsSource = dataPoints;
|
||||
|
||||
// 设置 Y 轴范围 | Set Y axis range
|
||||
UpdateYAxis(displayData, isLogarithmic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空图表,恢复初始状态 | Clear chart, restore initial state
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_areaSeries.ItemsSource = null;
|
||||
|
||||
// X 轴范围重置为 0-255 | Reset X axis range to 0-255
|
||||
_xAxis.Minimum = 0;
|
||||
_xAxis.Maximum = 255;
|
||||
|
||||
// Y 轴范围重置为 0-1 | Reset Y axis range to 0-1
|
||||
SetYAxisRange(0, 1, isLogarithmic: false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前数据点数量 | Get current data point count
|
||||
/// </summary>
|
||||
public int DataPointCount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_areaSeries.ItemsSource is ICollection<HistogramDataPoint> collection)
|
||||
return collection.Count;
|
||||
if (_areaSeries.ItemsSource is IEnumerable<HistogramDataPoint> enumerable)
|
||||
return enumerable.Count();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 65536 长度的频次数组聚合为 256 个柱体 | Aggregate 65536-length array to 256 bars
|
||||
/// </summary>
|
||||
private static long[] Aggregate16BitHistogram(long[] histogram)
|
||||
{
|
||||
var aggregated = new long[256];
|
||||
for (int i = 0; i < 256; i++)
|
||||
{
|
||||
long sum = 0;
|
||||
int startIndex = i * AggregationFactor;
|
||||
for (int j = 0; j < AggregationFactor; j++)
|
||||
{
|
||||
sum += histogram[startIndex + j];
|
||||
}
|
||||
aggregated[i] = sum;
|
||||
}
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新 Y 轴范围 | Update Y axis range
|
||||
/// </summary>
|
||||
private void UpdateYAxis(long[] displayData, bool isLogarithmic)
|
||||
{
|
||||
long maxValue = 0;
|
||||
for (int i = 0; i < displayData.Length; i++)
|
||||
{
|
||||
if (displayData[i] > maxValue)
|
||||
maxValue = displayData[i];
|
||||
}
|
||||
|
||||
if (maxValue == 0)
|
||||
maxValue = 1;
|
||||
|
||||
// 计算取整的 MajorStep(约 4 个刻度,对齐到 K/M 整数倍)| Calculate rounded MajorStep (~4 ticks, aligned to K/M multiples)
|
||||
if (_chart.VerticalAxis is LinearAxis linearAxis)
|
||||
{
|
||||
long rawStep = maxValue / 4;
|
||||
long step = RoundStepToNice(rawStep);
|
||||
if (step < 1) step = 1;
|
||||
linearAxis.MajorStep = step;
|
||||
|
||||
// 将最大值向上取整到 step 的整数倍 | Round max up to multiple of step
|
||||
long roundedMax = ((maxValue / step) + 1) * step;
|
||||
SetYAxisRange(0, roundedMax, isLogarithmic);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetYAxisRange(0, maxValue, isLogarithmic);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将步长取整为"好看"的数值(1, 2, 5 的倍数 × 10^n)| Round step to "nice" value (multiples of 1, 2, 5 × 10^n)
|
||||
/// 例如:123456 → 100000,350000 → 500000,780000 → 1000000
|
||||
/// </summary>
|
||||
private static long RoundStepToNice(long rawStep)
|
||||
{
|
||||
if (rawStep <= 0) return 1;
|
||||
|
||||
// 找到数量级 | Find order of magnitude
|
||||
double magnitude = Math.Pow(10, Math.Floor(Math.Log10(rawStep)));
|
||||
double normalized = rawStep / magnitude;
|
||||
|
||||
// 取整到 1, 2, 5 中最近的 | Round to nearest of 1, 2, 5
|
||||
double niceNormalized;
|
||||
if (normalized <= 1.5)
|
||||
niceNormalized = 1;
|
||||
else if (normalized <= 3.5)
|
||||
niceNormalized = 2;
|
||||
else if (normalized <= 7.5)
|
||||
niceNormalized = 5;
|
||||
else
|
||||
niceNormalized = 10;
|
||||
|
||||
return (long)(niceNormalized * magnitude);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 Y 轴范围和刻度类型 | Set Y axis range and scale type
|
||||
/// </summary>
|
||||
private void SetYAxisRange(double minimum, double maximum, bool isLogarithmic)
|
||||
{
|
||||
// 获取或创建 Y 轴 | Get or create Y axis
|
||||
var verticalAxis = _chart.VerticalAxis;
|
||||
|
||||
if (isLogarithmic)
|
||||
{
|
||||
// 对数刻度 | Logarithmic scale
|
||||
if (verticalAxis is LogarithmicAxis logAxis)
|
||||
{
|
||||
logAxis.Minimum = 1;
|
||||
logAxis.Maximum = maximum;
|
||||
logAxis.LogarithmBase = 10;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 需要切换为对数轴 | Need to switch to logarithmic axis
|
||||
var newLogAxis = new LogarithmicAxis
|
||||
{
|
||||
Minimum = 1,
|
||||
Maximum = maximum,
|
||||
LogarithmBase = 10
|
||||
};
|
||||
_chart.VerticalAxis = newLogAxis;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 线性刻度 | Linear scale
|
||||
if (verticalAxis is LinearAxis linearAxis)
|
||||
{
|
||||
linearAxis.Minimum = minimum;
|
||||
linearAxis.Maximum = maximum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 需要切换为线性轴 | Need to switch to linear axis
|
||||
var newLinearAxis = new LinearAxis
|
||||
{
|
||||
Minimum = minimum,
|
||||
Maximum = maximum
|
||||
};
|
||||
_chart.VerticalAxis = newLinearAxis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直方图数据点模型 | Histogram data point model
|
||||
/// </summary>
|
||||
internal class HistogramDataPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// 灰度级别(X 轴值)| Gray level (X axis value)
|
||||
/// </summary>
|
||||
public double GrayLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素频次(Y 轴值)| Pixel frequency (Y axis value)
|
||||
/// </summary>
|
||||
public long Frequency { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧率限流器,确保计算频率不超过 MaxFrameRate | Frame rate throttler
|
||||
/// 支持从任意线程调用,使用 lock 保护内部状态
|
||||
/// </summary>
|
||||
internal sealed class FrameThrottler : IDisposable
|
||||
{
|
||||
private readonly object _lock = new();
|
||||
private DateTime _lastProcessTime = DateTime.MinValue;
|
||||
private Action? _pendingAction;
|
||||
private CancellationTokenSource? _delayCts;
|
||||
private bool _isProcessing;
|
||||
private bool _disposed;
|
||||
private int _maxFrameRate = 15;
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率(fps),有效范围 1-60,超出范围自动钳位 | Max frame rate (fps), valid range 1-60, auto-clamped
|
||||
/// </summary>
|
||||
public int MaxFrameRate
|
||||
{
|
||||
get => _maxFrameRate;
|
||||
set => _maxFrameRate = Math.Clamp(value, 1, 60);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前帧间隔(毫秒)| Get current frame interval (ms)
|
||||
/// </summary>
|
||||
private double FrameIntervalMs => 1000.0 / _maxFrameRate;
|
||||
|
||||
/// <summary>
|
||||
/// 提交一帧计算动作 | Submit a frame compute action
|
||||
/// 若未超过帧率限制则立即执行,否则缓存最新帧并延迟触发
|
||||
/// </summary>
|
||||
/// <param name="computeAction">计算动作 | Compute action</param>
|
||||
/// <returns>是否被立即接受处理 | Whether it was immediately accepted</returns>
|
||||
public bool TrySubmit(Action computeAction)
|
||||
{
|
||||
if (_disposed || computeAction == null)
|
||||
return false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = (now - _lastProcessTime).TotalMilliseconds;
|
||||
|
||||
if (elapsed >= FrameIntervalMs && !_isProcessing)
|
||||
{
|
||||
// 已超过间隔且无正在处理的任务,立即执行 | Interval exceeded and no processing, execute immediately
|
||||
_isProcessing = true;
|
||||
_lastProcessTime = now;
|
||||
ExecuteAction(computeAction);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 未超过间隔或正在处理中,缓存最新帧(丢弃之前的中间帧)| Cache latest frame, discard previous
|
||||
_pendingAction = computeAction;
|
||||
ScheduleDelayedExecution(elapsed);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行计算动作(异步,完成后检查待处理帧)| Execute compute action asynchronously
|
||||
/// </summary>
|
||||
private void ExecuteAction(Action action)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action.Invoke();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 异常不外抛 | Do not propagate exceptions
|
||||
}
|
||||
finally
|
||||
{
|
||||
OnActionCompleted();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算动作完成后的回调 | Callback after compute action completes
|
||||
/// </summary>
|
||||
private void OnActionCompleted()
|
||||
{
|
||||
Action? nextAction = null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_isProcessing = false;
|
||||
|
||||
if (_pendingAction != null && !_disposed)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = (now - _lastProcessTime).TotalMilliseconds;
|
||||
|
||||
if (elapsed >= FrameIntervalMs)
|
||||
{
|
||||
// 间隔已到,立即执行待处理帧 | Interval reached, execute pending frame
|
||||
nextAction = _pendingAction;
|
||||
_pendingAction = null;
|
||||
_isProcessing = true;
|
||||
_lastProcessTime = now;
|
||||
}
|
||||
// 否则等待延迟触发 | Otherwise wait for delayed trigger
|
||||
}
|
||||
}
|
||||
|
||||
if (nextAction != null)
|
||||
{
|
||||
ExecuteAction(nextAction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 安排延迟执行(等待帧间隔到期后处理最新帧)| Schedule delayed execution
|
||||
/// </summary>
|
||||
private void ScheduleDelayedExecution(double elapsedMs)
|
||||
{
|
||||
// 取消之前的延迟任务 | Cancel previous delay task
|
||||
_delayCts?.Cancel();
|
||||
_delayCts?.Dispose();
|
||||
_delayCts = new CancellationTokenSource();
|
||||
var token = _delayCts.Token;
|
||||
|
||||
var delayMs = Math.Max(0, FrameIntervalMs - elapsedMs);
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay((int)delayMs, token);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
Action? actionToExecute = null;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_pendingAction != null && !_isProcessing && !_disposed)
|
||||
{
|
||||
actionToExecute = _pendingAction;
|
||||
_pendingAction = null;
|
||||
_isProcessing = true;
|
||||
_lastProcessTime = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
if (actionToExecute != null)
|
||||
{
|
||||
ExecuteAction(actionToExecute);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 延迟被取消,正常情况 | Delay cancelled, normal case
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 异常不外抛 | Do not propagate exceptions
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消所有待处理任务 | Cancel all pending tasks
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingAction = null;
|
||||
_delayCts?.Cancel();
|
||||
_delayCts?.Dispose();
|
||||
_delayCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放所有资源 | Dispose all resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_pendingAction = null;
|
||||
_delayCts?.Cancel();
|
||||
_delayCts?.Dispose();
|
||||
_delayCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 频次标签转换器:将大数值转为 K/M 缩写格式 | Frequency label converter: converts large values to K/M abbreviation format
|
||||
/// 例如:500000 → "500K",1500000 → "1.5M",800 → "800"
|
||||
/// </summary>
|
||||
internal sealed class FrequencyLabelConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null) return "0";
|
||||
|
||||
double num;
|
||||
if (value is double d)
|
||||
num = d;
|
||||
else if (value is decimal dec)
|
||||
num = (double)dec;
|
||||
else if (!double.TryParse(value.ToString(), out num))
|
||||
return value.ToString() ?? "0";
|
||||
|
||||
if (num >= 1_000_000)
|
||||
{
|
||||
double mValue = num / 1_000_000.0;
|
||||
return mValue == Math.Floor(mValue)
|
||||
? $"{(int)mValue}M"
|
||||
: $"{mValue:0.#}M";
|
||||
}
|
||||
|
||||
if (num >= 1_000)
|
||||
{
|
||||
double kValue = num / 1_000.0;
|
||||
return kValue == Math.Floor(kValue)
|
||||
? $"{(int)kValue}K"
|
||||
: $"{kValue:0.#}K";
|
||||
}
|
||||
|
||||
return $"{(int)num}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 直方图后台计算引擎 | Histogram background computation engine
|
||||
/// 负责在后台线程中执行灰度值遍历和统计计算
|
||||
/// </summary>
|
||||
internal sealed class HistogramEngine : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 单帧计算超时时间(毫秒)| Single frame computation timeout (ms)
|
||||
/// </summary>
|
||||
private const int ComputeTimeoutMs = 5000;
|
||||
|
||||
private CancellationTokenSource? _timeoutCts;
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 从 Image<Rgba32> 计算灰度直方图 | Compute histogram from Image<Rgba32>
|
||||
/// 使用 ITU-R BT.601 亮度公式:Gray = 0.299R + 0.587G + 0.114B
|
||||
/// </summary>
|
||||
/// <param name="image">输入图像 | Input image</param>
|
||||
/// <param name="ct">取消令牌 | Cancellation token</param>
|
||||
/// <returns>256 长度的频次数组,失败返回 null | 256-length frequency array, null on failure</returns>
|
||||
public Task<long[]?> ComputeAsync(Image<Rgba32> image, CancellationToken ct)
|
||||
{
|
||||
if (image == null)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
// 创建超时令牌 | Create timeout token
|
||||
var linkedCts = CreateLinkedTimeoutToken(ct);
|
||||
var linkedToken = linkedCts.Token;
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var width = image.Width;
|
||||
var height = image.Height;
|
||||
var histogram = new long[256];
|
||||
|
||||
// 遍历像素,使用亮度公式计算灰度值 | Iterate pixels, compute grayscale using luminance formula
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
linkedToken.ThrowIfCancellationRequested();
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
var pixel = image[x, y];
|
||||
// ITU-R BT.601 亮度公式 | ITU-R BT.601 luminance formula
|
||||
var gray = (int)(0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B);
|
||||
// 钳位到 0-255 范围 | Clamp to 0-255 range
|
||||
gray = Math.Clamp(gray, 0, 255);
|
||||
histogram[gray]++;
|
||||
}
|
||||
}
|
||||
|
||||
return (long[]?)histogram;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 超时或取消,返回 null | Timeout or cancelled, return null
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 所有异常内部捕获,不向外抛出 | Catch all exceptions internally
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
linkedCts.Dispose();
|
||||
}
|
||||
}, linkedToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从原始字节数组计算灰度直方图 | Compute histogram from raw byte array
|
||||
/// </summary>
|
||||
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
|
||||
/// <param name="width">图像宽度 | Image width</param>
|
||||
/// <param name="height">图像高度 | Image height</param>
|
||||
/// <param name="bitDepth">位深度(8 或 16)| Bit depth (8 or 16)</param>
|
||||
/// <param name="ct">取消令牌 | Cancellation token</param>
|
||||
/// <returns>频次数组(8位:256长度,16位:65536长度),失败返回 null | Frequency array, null on failure</returns>
|
||||
public Task<long[]?> ComputeAsync(byte[] rawData, int width, int height, int bitDepth, CancellationToken ct)
|
||||
{
|
||||
// 参数有效性验证 | Parameter validation
|
||||
if (rawData == null || width <= 0 || height <= 0)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
if (bitDepth != 8 && bitDepth != 16)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
|
||||
if (rawData.Length != expectedLength)
|
||||
return Task.FromResult<long[]?>(null);
|
||||
|
||||
// 创建超时令牌 | Create timeout token
|
||||
var linkedCts = CreateLinkedTimeoutToken(ct);
|
||||
var linkedToken = linkedCts.Token;
|
||||
|
||||
return Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (bitDepth == 8)
|
||||
{
|
||||
return ComputeHistogram8Bit(rawData, width, height, linkedToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ComputeHistogram16Bit(rawData, width, height, linkedToken);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
linkedCts.Dispose();
|
||||
}
|
||||
}, linkedToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 8 位灰度直方图 | Compute 8-bit grayscale histogram
|
||||
/// </summary>
|
||||
private static long[]? ComputeHistogram8Bit(byte[] rawData, int width, int height, CancellationToken ct)
|
||||
{
|
||||
var histogram = new long[256];
|
||||
int totalPixels = width * height;
|
||||
|
||||
for (int i = 0; i < totalPixels; i++)
|
||||
{
|
||||
if (i % 65536 == 0)
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
histogram[rawData[i]]++;
|
||||
}
|
||||
|
||||
return histogram;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算 16 位灰度直方图 | Compute 16-bit grayscale histogram
|
||||
/// </summary>
|
||||
private static long[]? ComputeHistogram16Bit(byte[] rawData, int width, int height, CancellationToken ct)
|
||||
{
|
||||
var histogram = new long[65536];
|
||||
int totalPixels = width * height;
|
||||
|
||||
for (int i = 0; i < totalPixels; i++)
|
||||
{
|
||||
if (i % 65536 == 0)
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// 小端序读取 16 位值 | Read 16-bit value in little-endian
|
||||
int offset = i * 2;
|
||||
ushort value = (ushort)(rawData[offset] | (rawData[offset + 1] << 8));
|
||||
histogram[value]++;
|
||||
}
|
||||
|
||||
return histogram;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带超时的链接取消令牌 | Create linked cancellation token with timeout
|
||||
/// </summary>
|
||||
private CancellationTokenSource CreateLinkedTimeoutToken(CancellationToken externalToken)
|
||||
{
|
||||
var timeoutCts = new CancellationTokenSource(ComputeTimeoutMs);
|
||||
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_timeoutCts?.Dispose();
|
||||
_timeoutCts = timeoutCts;
|
||||
}
|
||||
|
||||
return linkedCts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Dispose resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_timeoutCts?.Cancel();
|
||||
_timeoutCts?.Dispose();
|
||||
_timeoutCts = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<UserControl x:Class="XP.Common.Controls.ImageHistogram.ImageHistogramControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls.ImageHistogram"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="200" d:DesignWidth="400">
|
||||
<UserControl.Resources>
|
||||
<local:FrequencyLabelConverter x:Key="FreqConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<!-- 图表控件 | Chart control -->
|
||||
<telerik:RadCartesianChart x:Name="HistogramChart" Padding="0">
|
||||
<!-- 禁用 Telerik 自带的无数据提示 | Disable Telerik built-in empty content -->
|
||||
<telerik:RadCartesianChart.EmptyContent>
|
||||
<TextBlock/>
|
||||
</telerik:RadCartesianChart.EmptyContent>
|
||||
<!-- X 轴:灰度级别(缩小字体,控制刻度数量,K/M 缩写)| X Axis: Gray Level -->
|
||||
<telerik:RadCartesianChart.HorizontalAxis>
|
||||
<telerik:LinearAxis x:Name="XAxis"
|
||||
Minimum="0"
|
||||
Maximum="255"
|
||||
MajorStep="64"
|
||||
FontSize="9">
|
||||
<telerik:LinearAxis.LabelTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
|
||||
</DataTemplate>
|
||||
</telerik:LinearAxis.LabelTemplate>
|
||||
</telerik:LinearAxis>
|
||||
</telerik:RadCartesianChart.HorizontalAxis>
|
||||
|
||||
<!-- Y 轴:像素频次(K/M 缩写标签)| Y Axis: Pixel Frequency (K/M abbreviation labels) -->
|
||||
<telerik:RadCartesianChart.VerticalAxis>
|
||||
<telerik:LinearAxis x:Name="YAxis"
|
||||
Minimum="0"
|
||||
Maximum="1"
|
||||
FontSize="9">
|
||||
<telerik:LinearAxis.LabelTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Converter={StaticResource FreqConverter}}"/>
|
||||
</DataTemplate>
|
||||
</telerik:LinearAxis.LabelTemplate>
|
||||
</telerik:LinearAxis>
|
||||
</telerik:RadCartesianChart.VerticalAxis>
|
||||
|
||||
<!-- 面积图系列(适合密集直方图数据)| Area Series (suitable for dense histogram data) -->
|
||||
<telerik:RadCartesianChart.Series>
|
||||
<telerik:ScatterAreaSeries x:Name="HistogramBarSeries"
|
||||
XValueBinding="GrayLevel"
|
||||
YValueBinding="Frequency"
|
||||
Fill="#7F2196F3"
|
||||
Stroke="#FF2196F3"
|
||||
StrokeThickness="1"/>
|
||||
</telerik:RadCartesianChart.Series>
|
||||
</telerik:RadCartesianChart>
|
||||
|
||||
<!-- 无数据提示文本(叠加在图表上方)| No data placeholder text (overlaid on chart) -->
|
||||
<TextBlock x:Name="NoDataPlaceholder"
|
||||
Text="{loc:Localization Histogram_NoData}"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="#9E9E9E"
|
||||
Visibility="Visible"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Threading;
|
||||
using Prism.Ioc;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Controls.ImageHistogram
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像灰度直方图通用控件 | Image grayscale histogram control
|
||||
/// 支持单帧静态图像和高频流式图像输入,使用 Telerik RadChartView 进行可视化渲染
|
||||
/// </summary>
|
||||
public partial class ImageHistogramControl : UserControl
|
||||
{
|
||||
#region 依赖属性 | Dependency Properties
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率依赖属性 | MaxFrameRate dependency property
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty MaxFrameRateProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(MaxFrameRate),
|
||||
typeof(int),
|
||||
typeof(ImageHistogramControl),
|
||||
new PropertyMetadata(15, OnMaxFrameRateChanged, CoerceMaxFrameRate));
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用对数 Y 轴依赖属性 | IsLogarithmic dependency property
|
||||
/// </summary>
|
||||
public static readonly DependencyProperty IsLogarithmicProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(IsLogarithmic),
|
||||
typeof(bool),
|
||||
typeof(ImageHistogramControl),
|
||||
new PropertyMetadata(false));
|
||||
|
||||
/// <summary>
|
||||
/// 最大刷新帧率(fps),有效范围 1-60,默认 15 | Max frame rate (fps), valid range 1-60, default 15
|
||||
/// </summary>
|
||||
public int MaxFrameRate
|
||||
{
|
||||
get => (int)GetValue(MaxFrameRateProperty);
|
||||
set => SetValue(MaxFrameRateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用对数 Y 轴,默认 false | Whether to use logarithmic Y axis, default false
|
||||
/// </summary>
|
||||
public bool IsLogarithmic
|
||||
{
|
||||
get => (bool)GetValue(IsLogarithmicProperty);
|
||||
set => SetValue(IsLogarithmicProperty, value);
|
||||
}
|
||||
|
||||
private static void OnMaxFrameRateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageHistogramControl control)
|
||||
{
|
||||
var newValue = (int)e.NewValue;
|
||||
control._frameThrottler.MaxFrameRate = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static object CoerceMaxFrameRate(DependencyObject d, object baseValue)
|
||||
{
|
||||
var value = (int)baseValue;
|
||||
var clamped = Math.Clamp(value, 1, 60);
|
||||
|
||||
if (clamped != value && d is ImageHistogramControl control)
|
||||
{
|
||||
control._logger?.Warn(
|
||||
"MaxFrameRate 值 {Value} 超出有效范围,已钳位为 {Clamped} | MaxFrameRate value {Value} out of range, clamped to {Clamped}",
|
||||
value, clamped);
|
||||
}
|
||||
|
||||
return clamped;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有字段 | Private Fields
|
||||
|
||||
private readonly FrameThrottler _frameThrottler;
|
||||
private readonly HistogramEngine _histogramEngine;
|
||||
private ChartRenderer? _chartRenderer;
|
||||
private ILoggerService? _logger;
|
||||
private CancellationTokenSource? _currentCts;
|
||||
private readonly object _ctsLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数 | Constructor
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
public ImageHistogramControl()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// 初始化内部组件 | Initialize internal components
|
||||
_frameThrottler = new FrameThrottler();
|
||||
_histogramEngine = new HistogramEngine();
|
||||
|
||||
// 尝试解析日志服务 | Try to resolve logger service
|
||||
try
|
||||
{
|
||||
var loggerService = ContainerLocator.Current?.Resolve<ILoggerService>();
|
||||
_logger = loggerService?.ForModule<ImageHistogramControl>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 日志服务不可用,静默降级 | Logger service unavailable, silent degradation
|
||||
_logger = null;
|
||||
}
|
||||
|
||||
// 订阅 Loaded 事件初始化 ChartRenderer | Subscribe Loaded event to initialize ChartRenderer
|
||||
Loaded += OnLoaded;
|
||||
Unloaded += OnUnloaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 初始化 ChartRenderer | Initialize ChartRenderer
|
||||
_chartRenderer = new ChartRenderer(HistogramChart, HistogramBarSeries, XAxis);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共 API | Public API
|
||||
|
||||
/// <summary>
|
||||
/// 传入 ImageSharp 图像对象,计算并显示灰度直方图 | Update histogram from ImageSharp image
|
||||
/// </summary>
|
||||
/// <param name="image">ImageSharp 图像对象 | ImageSharp image object</param>
|
||||
public void UpdateImage(Image<Rgba32> image)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (image == null)
|
||||
{
|
||||
_logger?.Warn("UpdateImage 收到 null 图像,已忽略 | UpdateImage received null image, ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
SubmitComputation(() => _histogramEngine.ComputeAsync(image, GetOrCreateCancellationToken()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "UpdateImage(Image) 异常:{Message} | UpdateImage(Image) error: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 传入原始像素数组,计算并显示灰度直方图 | Update histogram from raw byte array
|
||||
/// </summary>
|
||||
/// <param name="rawData">原始像素数据 | Raw pixel data</param>
|
||||
/// <param name="width">图像宽度 | Image width</param>
|
||||
/// <param name="height">图像高度 | Image height</param>
|
||||
/// <param name="bitDepth">位深度(8 或 16)| Bit depth (8 or 16)</param>
|
||||
public void UpdateImage(byte[] rawData, int width, int height, int bitDepth)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 参数有效性验证 | Parameter validation
|
||||
if (rawData == null)
|
||||
{
|
||||
_logger?.Warn("UpdateImage 收到 null rawData,已忽略 | UpdateImage received null rawData, ignored");
|
||||
return;
|
||||
}
|
||||
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
_logger?.Warn(
|
||||
"UpdateImage 参数无效:width={Width}, height={Height} | Invalid params: width={Width}, height={Height}",
|
||||
width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bitDepth != 8 && bitDepth != 16)
|
||||
{
|
||||
_logger?.Warn(
|
||||
"UpdateImage 参数无效:bitDepth={BitDepth},仅支持 8 或 16 | Invalid bitDepth={BitDepth}, only 8 or 16 supported",
|
||||
bitDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
int expectedLength = bitDepth == 8 ? width * height : width * height * 2;
|
||||
if (rawData.Length != expectedLength)
|
||||
{
|
||||
_logger?.Warn(
|
||||
"UpdateImage 参数无效:rawData.Length={Length}, 预期={Expected} | Invalid params: rawData.Length={Length}, expected={Expected}",
|
||||
rawData.Length, expectedLength);
|
||||
return;
|
||||
}
|
||||
|
||||
SubmitComputation(() => _histogramEngine.ComputeAsync(rawData, width, height, bitDepth, GetOrCreateCancellationToken()));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "UpdateImage(byte[]) 异常:{Message} | UpdateImage(byte[]) error: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空直方图显示,恢复初始空白状态 | Clear histogram display, restore initial blank state
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 取消正在执行的后台任务 | Cancel running background task
|
||||
CancelCurrentComputation();
|
||||
|
||||
// 取消帧率限流器中的待处理任务 | Cancel pending tasks in throttler
|
||||
_frameThrottler.Cancel();
|
||||
|
||||
// 清空图表(捕获局部引用避免异步执行时为 null)| Clear chart (capture local ref to avoid null during async)
|
||||
var renderer = _chartRenderer;
|
||||
if (renderer != null)
|
||||
{
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
renderer.Clear();
|
||||
|
||||
// 显示无数据提示 | Show no-data placeholder
|
||||
NoDataPlaceholder.Visibility = Visibility.Visible;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 控件已卸载时忽略 | Ignore if control already unloaded
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "Clear() 异常:{Message} | Clear() error: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法 | Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// 通过帧率限流器提交计算任务 | Submit computation through frame throttler
|
||||
/// </summary>
|
||||
private void SubmitComputation(Func<System.Threading.Tasks.Task<long[]?>> computeFunc)
|
||||
{
|
||||
_frameThrottler.TrySubmit(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var task = computeFunc();
|
||||
task.ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompletedSuccessfully && t.Result != null)
|
||||
{
|
||||
var histogram = t.Result;
|
||||
var isLog = false;
|
||||
|
||||
// 在 UI 线程获取 IsLogarithmic 值并更新图表 | Get IsLogarithmic on UI thread and update chart
|
||||
Dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
isLog = IsLogarithmic;
|
||||
_chartRenderer?.UpdateData(histogram, isLog);
|
||||
|
||||
// 隐藏无数据提示 | Hide no-data placeholder
|
||||
NoDataPlaceholder.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "图表更新异常:{Message} | Chart update error: {Message}", ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
else if (t.IsFaulted)
|
||||
{
|
||||
_logger?.Error(t.Exception, "直方图计算异常:{Message} | Histogram computation error: {Message}",
|
||||
t.Exception?.InnerException?.Message ?? "Unknown");
|
||||
}
|
||||
}, System.Threading.Tasks.TaskScheduler.Default);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Error(ex, "提交计算任务异常:{Message} | Submit computation error: {Message}", ex.Message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或创建取消令牌(取消上一个)| Get or create cancellation token (cancel previous)
|
||||
/// </summary>
|
||||
private CancellationToken GetOrCreateCancellationToken()
|
||||
{
|
||||
lock (_ctsLock)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_currentCts = new CancellationTokenSource();
|
||||
return _currentCts.Token;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消当前计算 | Cancel current computation
|
||||
/// </summary>
|
||||
private void CancelCurrentComputation()
|
||||
{
|
||||
lock (_ctsLock)
|
||||
{
|
||||
_currentCts?.Cancel();
|
||||
_currentCts?.Dispose();
|
||||
_currentCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unloaded 事件处理:释放所有资源 | Unloaded event handler: release all resources
|
||||
/// </summary>
|
||||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 取消所有后台任务 | Cancel all background tasks
|
||||
CancelCurrentComputation();
|
||||
|
||||
// 释放帧率限流器 | Dispose frame throttler
|
||||
_frameThrottler.Cancel();
|
||||
_frameThrottler.Dispose();
|
||||
|
||||
// 释放计算引擎 | Dispose histogram engine
|
||||
_histogramEngine.Dispose();
|
||||
|
||||
// 清空引用 | Clear references
|
||||
_chartRenderer = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆核心计算逻辑(纯函数,无副作用)| Virtual joystick core calculation logic (pure functions, no side effects)
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆轴模式枚举 | Virtual joystick axis mode enumeration
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 鼠标按键类型枚举 | Mouse button type enumeration
|
||||
+1
-1
@@ -6,7 +6,7 @@ using System.Windows.Media;
|
||||
using System.Windows.Shapes;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Controls
|
||||
namespace XP.Common.Controls.Joystick
|
||||
{
|
||||
/// <summary>
|
||||
/// 虚拟摇杆 UserControl,提供圆形区域内的鼠标拖拽操控能力 | Virtual joystick UserControl providing mouse drag interaction within a circular area
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
<UserControl x:Class="XP.Common.Controls.VirtualJoystick"
|
||||
<UserControl x:Class="XP.Common.Controls.Joystick.VirtualJoystick"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls"
|
||||
xmlns:local="clr-namespace:XP.Common.Controls.Joystick"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="200" d:DesignHeight="200"
|
||||
Cursor="Hand">
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Configuration;
|
||||
|
||||
namespace XP.Common.Database.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite 配置加载器,从 App.config 读取数据库相关配置项 | SQLite configuration loader, reads database-related configuration from App.config
|
||||
/// </summary>
|
||||
public static class ConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置键前缀 | Configuration key prefix
|
||||
/// </summary>
|
||||
private const string KeyPrefix = "Sqlite:";
|
||||
|
||||
/// <summary>
|
||||
/// 从 App.config 加载 SQLite 配置 | Load SQLite configuration from App.config
|
||||
/// </summary>
|
||||
/// <returns>SQLite 配置实体,缺失或无效配置项使用默认值 | SQLite configuration entity, uses default values for missing or invalid items</returns>
|
||||
public static SqliteConfig LoadSqliteConfig()
|
||||
{
|
||||
var config = new SqliteConfig();
|
||||
|
||||
// 加载数据库文件路径 | Load database file path
|
||||
var dbPath = ConfigurationManager.AppSettings[KeyPrefix + "DbFilePath"];
|
||||
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
|
||||
|
||||
// 加载连接超时时间 | Load connection timeout
|
||||
var timeout = ConfigurationManager.AppSettings[KeyPrefix + "ConnectionTimeout"];
|
||||
if (int.TryParse(timeout, out var t) && t > 0) config.ConnectionTimeout = t;
|
||||
|
||||
// 加载是否自动创建 | Load create if not exists
|
||||
var createIfNotExists = ConfigurationManager.AppSettings[KeyPrefix + "CreateIfNotExists"];
|
||||
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
|
||||
|
||||
// 加载是否启用 WAL 模式 | Load enable WAL mode
|
||||
var enableWal = ConfigurationManager.AppSettings[KeyPrefix + "EnableWalMode"];
|
||||
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
|
||||
|
||||
// 加载是否开启 SQL 日志 | Load enable SQL logging
|
||||
var enableSqlLog = ConfigurationManager.AppSettings[KeyPrefix + "EnableSqlLogging"];
|
||||
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,42 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Configs
|
||||
namespace XP.Common.Database.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite 配置实体
|
||||
/// SQLite 配置实体(从 App.config 读取)| SQLite configuration entity (loaded from App.config)
|
||||
/// </summary>
|
||||
public class SqliteConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库文件路径
|
||||
/// 数据库文件路径 | Database file path
|
||||
/// </summary>
|
||||
public string DbFilePath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Files", "Data", "XP.db");
|
||||
|
||||
/// <summary>
|
||||
/// 连接超时时间(秒,默认30)
|
||||
/// 连接超时时间(秒,默认30)| Connection timeout (seconds, default 30)
|
||||
/// </summary>
|
||||
public int ConnectionTimeout { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库不存在时是否自动创建(默认true)
|
||||
/// 数据库不存在时是否自动创建(默认true)| Whether to auto-create if not exists (default true)
|
||||
/// </summary>
|
||||
public bool CreateIfNotExists { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用 WAL 模式(提升并发性能,默认true)
|
||||
/// 是否启用 WAL 模式(提升并发性能,默认true)| Whether to enable WAL mode (default true)
|
||||
/// </summary>
|
||||
public bool EnableWalMode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启日志记录(记录所有SQL操作,默认false)
|
||||
/// 是否开启日志记录(记录所有SQL操作,默认false)| Whether to enable SQL logging (default false)
|
||||
/// </summary>
|
||||
public bool EnableSqlLogging { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取SQLite连接字符串
|
||||
/// 获取 SQLite 连接字符串 | Get SQLite connection string
|
||||
/// </summary>
|
||||
public string GetConnectionString()
|
||||
{
|
||||
@@ -7,7 +7,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Database.Configs;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.Database.Models;
|
||||
using XP.Common.Helpers;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# 授权服务使用指南 | License Service Usage Guide
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
XplorePlane 通过 `XP.Common.License` 命名空间下的 `ILicenseService` 提供统一的授权管理。底层基于海克斯康 CLMS(Computational License Management System)SDK,使用 `MORCODE.dll` 进行许可证校验。
|
||||
|
||||
## 产品授权信息 | Product License Information
|
||||
|
||||
| 项目 | 值 |
|
||||
| --- | --- |
|
||||
| CLMS 模块 ID(Module ID) | `4` |
|
||||
| 零件号(Part Number) | `LS950-0071-5-1` |
|
||||
|
||||
`Module ID` 是 CLMS 在 SDK 中标识本产品的唯一编号,调用 `CLM_ModuleIsLicensed` 时必须传入此值;`Part Number` 是产品在 CLMS 许可发行系统中的物料编号,用于申请、签发、续期许可证。
|
||||
|
||||
## 授权模式 | License Modes
|
||||
|
||||
`LicenseMode` 枚举定义见 `XP.Common.License.Enums.LicenseMode`:
|
||||
|
||||
| 模式 | 枚举值 | 数值 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| CLMS 正式授权 | `Clms` | `0` | 通过 CLMS SDK 进行完整授权校验,包含登录、许可范围、模块、SMA、到期日期等检查 |
|
||||
| 临时测试模式 | `TemporaryTest` | `885` | 不调用 CLMS SDK,直接放行 15 分钟,用于研发/测试场景 |
|
||||
|
||||
## 配置项 | Configuration Items
|
||||
|
||||
授权配置位于主应用 `App.config` 中,键前缀为 `License:`,对应 `XP.Common.License.Configs.LicenseConfig`:
|
||||
|
||||
```xml
|
||||
<appSettings>
|
||||
<!-- 授权配置 | License configuration -->
|
||||
<add key="License:LicenseMode" value="0" /> <!-- 授权模式:0=CLMS 正式授权,885=临时测试模式 -->
|
||||
<add key="License:ModuleId" value="4" /> <!-- 模块 ID,XplorePlane 固定为 4 -->
|
||||
<add key="License:UseSma" value="false" /> <!-- 是否启用 SMA 检查 -->
|
||||
<add key="License:LicenseState" value="20" /> <!-- 上次授权状态:10=成功,20=失败(运行时由 LicenseService 自动写回)-->
|
||||
</appSettings>
|
||||
```
|
||||
|
||||
| 键 | 类型 | 默认值 | 有效值 | 说明 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `License:LicenseMode` | int | `0` | `0`、`885` | 授权模式 |
|
||||
| `License:ModuleId` | ushort | `4` | `1` ~ `65535` | CLMS 模块 ID |
|
||||
| `License:UseSma` | bool | `false` | `true`、`false` | 是否启用 SMA(软件维护协议)校验 |
|
||||
| `License:LicenseState` | int | `20` | `10`、`20` | 上次运行时的授权结果,由服务自动维护 |
|
||||
|
||||
> 配置中无效或缺失的键会回退到默认值,详见 `XP.Common.License.Configs.ConfigLoader`。
|
||||
|
||||
## 正式授权流程 | Formal Authorization Flow
|
||||
|
||||
`LicenseMode = 0`(`Clms`)时,`LicenseService.CheckAuthorization()` 依次执行以下步骤:
|
||||
|
||||
1. **系统时间检查**(可选):调用 `CLM_CheckSystemTime`,老版本 SDK 缺失此入口点时跳过。
|
||||
2. **登录验证**:调用 `CLM_Login`(核心步骤,必须存在)。
|
||||
3. **许可范围检查**:调用 `CLM_Login_Scope`(核心步骤)。
|
||||
4. **SMA 验证**(可选):仅当 `License:UseSma = true` 时执行。比较 SMA 年份/季度与当前软件主版本号/次版本号,季度不匹配会判定失败。
|
||||
5. **浮动许可信息**(可选):通过 `CLM_GetIP` 获取 IP 与端口。
|
||||
6. **错误信息读取**(可选):通过 `CLM_GetError` 拉取 SDK 端错误描述。
|
||||
7. **模块授权检查**:调用 `CLM_ModuleIsLicensed`,传入 `Module ID = 4`。
|
||||
8. **到期日期获取**:调用 `CLM_GetWarrantyExpiration`,剩余 ≤ 30 天时记录警告。
|
||||
|
||||
任意核心步骤失败即视为授权失败,写回 `License:LicenseState = 20`,主应用弹窗提示并退出。
|
||||
|
||||
## 临时测试模式 | Temporary Test Mode
|
||||
|
||||
`LicenseMode = 885`(`TemporaryTest`)用于跳过 CLMS 校验,便于离线开发和功能演示。
|
||||
|
||||
### 行为 | Behavior
|
||||
|
||||
- 不加载 `MORCODE.dll`,不调用任何 CLMS API,授权直接通过。
|
||||
- 启动后立刻开启 15 分钟(900 秒)倒计时,到期触发 `TestModeTimeout` 事件,主应用应执行优雅关闭。
|
||||
- 倒计时途中分别在剩余 5 分钟、1 分钟时触发 `TestModeWarning5Min`、`TestModeWarning1Min` 事件用于提醒。
|
||||
- 上述三个事件只在临时测试模式下被触发,正式授权(`Clms`)下不会创建计时器。
|
||||
|
||||
### 启用方式 | How to Enable
|
||||
|
||||
修改主应用 `App.config`:
|
||||
|
||||
```xml
|
||||
<add key="License:LicenseMode" value="885" />
|
||||
```
|
||||
|
||||
启动后将看到提示:「当前为临时测试模式,软件将在 15 分钟后自动关闭」。
|
||||
|
||||
> **注意**:临时测试模式仅用于研发与内部测试场景,**禁止用于生产或交付**。发布前请务必将 `License:LicenseMode` 还原为 `0`。
|
||||
|
||||
## 使用示例 | Usage Examples
|
||||
|
||||
### 注入并校验 | Inject and Verify
|
||||
|
||||
```csharp
|
||||
using XP.Common.License.Interfaces;
|
||||
using XP.Common.License.Enums;
|
||||
|
||||
public class StartupChecker
|
||||
{
|
||||
private readonly ILicenseService _licenseService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public StartupChecker(ILicenseService licenseService, ILoggerService logger)
|
||||
{
|
||||
_licenseService = licenseService ?? throw new ArgumentNullException(nameof(licenseService));
|
||||
_logger = logger?.ForModule<StartupChecker>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public bool Run()
|
||||
{
|
||||
var result = _licenseService.CheckAuthorization();
|
||||
if (!result.IsAuthorized)
|
||||
{
|
||||
_logger.Error(null, "授权失败:{Message} | License failed: {Message}", result.Message);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 仅在临时测试模式下订阅倒计时事件 | Subscribe countdown events only in test mode
|
||||
if (_licenseService.LicenseMode == LicenseMode.TemporaryTest)
|
||||
{
|
||||
_licenseService.TestModeWarning5Min += (s, e) => _logger.Warn("临时测试模式剩余 5 分钟");
|
||||
_licenseService.TestModeWarning1Min += (s, e) => _logger.Warn("临时测试模式剩余 1 分钟");
|
||||
_licenseService.TestModeTimeout += (s, e) => Application.Current.Shutdown();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 检查特定模块授权 | Check Module Authorization
|
||||
|
||||
```csharp
|
||||
const ushort XplorePlaneModuleId = 4;
|
||||
if (!_licenseService.IsModuleLicensed(XplorePlaneModuleId))
|
||||
{
|
||||
_logger.Warn("模块 {ModuleId} 未授权 | Module {ModuleId} not licensed", XplorePlaneModuleId);
|
||||
}
|
||||
```
|
||||
|
||||
### 读取授权信息 | Read License Information
|
||||
|
||||
```csharp
|
||||
DateTime? expiration = _licenseService.GetExpirationDate(); // 授权到期日期
|
||||
DateTime? sma = _licenseService.GetSmaDate(); // SMA 到期日期
|
||||
int remaining = _licenseService.GetRemainingTestTime(); // 临时测试剩余秒数;非测试模式返回 -1
|
||||
```
|
||||
|
||||
## 接口与事件 | Interface & Events
|
||||
|
||||
`ILicenseService` 提供以下成员(详见 `XP.Common.License.Interfaces.ILicenseService`):
|
||||
|
||||
- `LicenseCheckResult CheckAuthorization()`:执行完整授权校验。
|
||||
- `bool IsAuthorized`:当前会话是否已授权。
|
||||
- `LicenseMode LicenseMode`:当前授权模式。
|
||||
- `DateTime? GetExpirationDate()`:授权到期日期。
|
||||
- `DateTime? GetSmaDate()`:SMA 到期日期。
|
||||
- `bool IsModuleLicensed(ushort moduleId)`:检查指定模块是否被授权。
|
||||
- `int GetRemainingTestTime()`:临时测试模式剩余秒数。
|
||||
- 事件:`TestModeWarning5Min`、`TestModeWarning1Min`、`TestModeTimeout`。
|
||||
|
||||
## 常见问题 | FAQ
|
||||
|
||||
### 1. 启动时提示 `MORCODE.dll 加载失败` | `Failed to load MORCODE.dll`
|
||||
|
||||
确认 `MORCODE.dll` 已随 `ReleaseFiles` 一同部署,并位于主程序同级目录。研发阶段可临时切换到临时测试模式绕过。
|
||||
|
||||
### 2. 提示「模块号码 4 不可用」| `Module 4 unavailable`
|
||||
|
||||
CLMS 服务器签发的许可证未包含 `Module ID = 4`(零件号 `LS950-0071-5-1`)。请联系海克斯康许可团队确认许可范围。
|
||||
|
||||
### 3. SMA 校验失败 | SMA validation failed
|
||||
|
||||
SMA 年份必须 ≥ 软件主版本号;同年时 SMA 季度必须 ≥ 软件次版本号。若需临时绕过,可将 `License:UseSma` 设为 `false`,但发布版本仍应保持 SMA 校验启用。
|
||||
|
||||
### 4. 授权成功后是否还有倒计时?| Will the countdown still trigger after a successful formal authorization?
|
||||
|
||||
不会。倒计时仅在 `LicenseMode = TemporaryTest` 时启动,`Clms` 正式授权下三个事件永远不会被触发。
|
||||
|
||||
## 相关文件 | Related Files
|
||||
|
||||
- `XP.Common/License/Interfaces/ILicenseService.cs` — 服务接口
|
||||
- `XP.Common/License/Implementations/LicenseService.cs` — 服务实现
|
||||
- `XP.Common/License/Configs/LicenseConfig.cs` — 配置实体
|
||||
- `XP.Common/License/Configs/ConfigLoader.cs` — 配置加载器
|
||||
- `XP.Common/License/Enums/LicenseMode.cs` — 授权模式枚举
|
||||
- `XP.Common/License/Enums/LicenseState.cs` — 授权状态枚举
|
||||
- `XP.Common/License/Native/NativeMethods.cs` — CLMS SDK P/Invoke 封装
|
||||
- `XplorePlane/App.xaml.cs` — 主应用启动时调用 `PerformLicenseCheck()` 的入口
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Configuration;
|
||||
|
||||
namespace XP.Common.Dump.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 配置加载器,从 App.config 读取 Dump 相关配置项 | Dump configuration loader, reads dump-related configuration from App.config
|
||||
/// </summary>
|
||||
public static class ConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置键前缀 | Configuration key prefix
|
||||
/// </summary>
|
||||
private const string KeyPrefix = "Dump:";
|
||||
|
||||
/// <summary>
|
||||
/// 从 App.config 加载 Dump 配置 | Load Dump configuration from App.config
|
||||
/// </summary>
|
||||
/// <returns>Dump 配置实体,缺失或无效配置项使用默认值 | Dump configuration entity, uses default values for missing or invalid items</returns>
|
||||
public static DumpConfig LoadDumpConfig()
|
||||
{
|
||||
var config = new DumpConfig();
|
||||
|
||||
// 加载存储路径 | Load storage path
|
||||
var storagePath = ConfigurationManager.AppSettings[KeyPrefix + "StoragePath"];
|
||||
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
|
||||
|
||||
// 加载是否启用定时触发 | Load enable scheduled dump
|
||||
var enableScheduled = ConfigurationManager.AppSettings[KeyPrefix + "EnableScheduledDump"];
|
||||
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
|
||||
|
||||
// 加载定时触发间隔 | Load scheduled interval
|
||||
var interval = ConfigurationManager.AppSettings[KeyPrefix + "ScheduledIntervalMinutes"];
|
||||
if (int.TryParse(interval, out var min) && min > 0) config.ScheduledIntervalMinutes = min;
|
||||
|
||||
// 加载 Mini Dump 文件大小上限 | Load Mini Dump size limit
|
||||
var sizeLimit = ConfigurationManager.AppSettings[KeyPrefix + "MiniDumpSizeLimitMB"];
|
||||
if (long.TryParse(sizeLimit, out var size) && size > 0) config.MiniDumpSizeLimitMB = size;
|
||||
|
||||
// 加载文件保留天数 | Load retention days
|
||||
var retentionDays = ConfigurationManager.AppSettings[KeyPrefix + "RetentionDays"];
|
||||
if (int.TryParse(retentionDays, out var days) && days > 0) config.RetentionDays = days;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System.Configuration;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Dump.Configs;
|
||||
|
||||
namespace XP.Common.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用配置加载工具(读取App.config)
|
||||
/// </summary>
|
||||
public static class ConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载Serilog配置
|
||||
/// </summary>
|
||||
public static SerilogConfig LoadSerilogConfig()
|
||||
{
|
||||
var config = new SerilogConfig();
|
||||
|
||||
var logPath = ConfigurationManager.AppSettings["Serilog:LogPath"];
|
||||
if (!string.IsNullOrEmpty(logPath)) config.LogPath = logPath;
|
||||
|
||||
var minLevel = ConfigurationManager.AppSettings["Serilog:MinimumLevel"];
|
||||
if (!string.IsNullOrEmpty(minLevel)) config.MinimumLevel = minLevel;
|
||||
|
||||
var enableConsole = ConfigurationManager.AppSettings["Serilog:EnableConsole"];
|
||||
if (bool.TryParse(enableConsole, out var console)) config.EnableConsole = console;
|
||||
|
||||
var rollingInterval = ConfigurationManager.AppSettings["Serilog:RollingInterval"];
|
||||
if (!string.IsNullOrEmpty(rollingInterval)) config.RollingInterval = rollingInterval;
|
||||
|
||||
var fileSize = ConfigurationManager.AppSettings["Serilog:FileSizeLimitMB"];
|
||||
if (long.TryParse(fileSize, out var size)) config.FileSizeLimitMB = size;
|
||||
|
||||
var retainCount = ConfigurationManager.AppSettings["Serilog:RetainedFileCountLimit"];
|
||||
if (int.TryParse(retainCount, out var count)) config.RetainedFileCountLimit = count;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载SQLite配置
|
||||
/// </summary>
|
||||
public static SqliteConfig LoadSqliteConfig()
|
||||
{
|
||||
var config = new SqliteConfig();
|
||||
|
||||
var dbPath = ConfigurationManager.AppSettings["Sqlite:DbFilePath"];
|
||||
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
|
||||
|
||||
var timeout = ConfigurationManager.AppSettings["Sqlite:ConnectionTimeout"];
|
||||
if (int.TryParse(timeout, out var t)) config.ConnectionTimeout = t;
|
||||
|
||||
var createIfNotExists = ConfigurationManager.AppSettings["Sqlite:CreateIfNotExists"];
|
||||
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
|
||||
|
||||
var enableWal = ConfigurationManager.AppSettings["Sqlite:EnableWalMode"];
|
||||
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
|
||||
|
||||
var enableSqlLog = ConfigurationManager.AppSettings["Sqlite:EnableSqlLogging"];
|
||||
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Dump 配置 | Load Dump configuration
|
||||
/// </summary>
|
||||
public static DumpConfig LoadDumpConfig()
|
||||
{
|
||||
var config = new DumpConfig();
|
||||
|
||||
var storagePath = ConfigurationManager.AppSettings["Dump:StoragePath"];
|
||||
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
|
||||
|
||||
var enableScheduled = ConfigurationManager.AppSettings["Dump:EnableScheduledDump"];
|
||||
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
|
||||
|
||||
var interval = ConfigurationManager.AppSettings["Dump:ScheduledIntervalMinutes"];
|
||||
if (int.TryParse(interval, out var min)) config.ScheduledIntervalMinutes = min;
|
||||
|
||||
var sizeLimit = ConfigurationManager.AppSettings["Dump:MiniDumpSizeLimitMB"];
|
||||
if (long.TryParse(sizeLimit, out var size)) config.MiniDumpSizeLimitMB = size;
|
||||
|
||||
var retentionDays = ConfigurationManager.AppSettings["Dump:RetentionDays"];
|
||||
if (int.TryParse(retentionDays, out var days)) config.RetentionDays = days;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Configuration;
|
||||
|
||||
namespace XP.Common.License.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// 授权配置加载器,从 App.config 读取授权相关配置项 | License configuration loader, reads license-related configuration from App.config
|
||||
/// </summary>
|
||||
public static class ConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置键前缀 | Configuration key prefix
|
||||
/// </summary>
|
||||
private const string KeyPrefix = "License:";
|
||||
|
||||
/// <summary>
|
||||
/// LicenseMode 有效值集合 | Valid values for LicenseMode
|
||||
/// </summary>
|
||||
private static readonly int[] ValidLicenseModes = { 0, 885 };
|
||||
|
||||
/// <summary>
|
||||
/// LicenseState 有效值集合 | Valid values for LicenseState
|
||||
/// </summary>
|
||||
private static readonly int[] ValidLicenseStates = { 10, 20 };
|
||||
|
||||
/// <summary>
|
||||
/// 从 App.config 加载授权配置 | Load license configuration from App.config
|
||||
/// </summary>
|
||||
/// <returns>授权配置实体,缺失或无效配置项使用默认值 | License configuration entity, uses default values for missing or invalid items</returns>
|
||||
public static LicenseConfig LoadLicenseConfig()
|
||||
{
|
||||
var config = new LicenseConfig();
|
||||
|
||||
// 加载 LicenseMode | Load LicenseMode
|
||||
var licenseModeStr = ConfigurationManager.AppSettings[KeyPrefix + "LicenseMode"];
|
||||
if (int.TryParse(licenseModeStr, out var licenseMode) && IsValidLicenseMode(licenseMode))
|
||||
{
|
||||
config.LicenseMode = licenseMode;
|
||||
}
|
||||
|
||||
// 加载 ModuleId | Load ModuleId
|
||||
var moduleIdStr = ConfigurationManager.AppSettings[KeyPrefix + "ModuleId"];
|
||||
if (ushort.TryParse(moduleIdStr, out var moduleId) && IsValidModuleId(moduleId))
|
||||
{
|
||||
config.ModuleId = moduleId;
|
||||
}
|
||||
|
||||
// 加载 UseSma | Load UseSma
|
||||
var useSmaStr = ConfigurationManager.AppSettings[KeyPrefix + "UseSma"];
|
||||
if (bool.TryParse(useSmaStr, out var useSma))
|
||||
{
|
||||
config.UseSma = useSma;
|
||||
}
|
||||
|
||||
// 加载 LicenseState | Load LicenseState
|
||||
var licenseStateStr = ConfigurationManager.AppSettings[KeyPrefix + "LicenseState"];
|
||||
if (int.TryParse(licenseStateStr, out var licenseState) && IsValidLicenseState(licenseState))
|
||||
{
|
||||
config.LicenseState = licenseState;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 LicenseMode 值是否有效 | Validate whether LicenseMode value is valid
|
||||
/// </summary>
|
||||
/// <param name="value">待验证的值 | Value to validate</param>
|
||||
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
|
||||
private static bool IsValidLicenseMode(int value)
|
||||
{
|
||||
foreach (var valid in ValidLicenseModes)
|
||||
{
|
||||
if (value == valid) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 ModuleId 值是否在有效范围内 | Validate whether ModuleId value is within valid range
|
||||
/// </summary>
|
||||
/// <param name="value">待验证的值 | Value to validate</param>
|
||||
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
|
||||
private static bool IsValidModuleId(ushort value)
|
||||
{
|
||||
return value >= 1 && value <= 65535;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 LicenseState 值是否有效 | Validate whether LicenseState value is valid
|
||||
/// </summary>
|
||||
/// <param name="value">待验证的值 | Value to validate</param>
|
||||
/// <returns>true 表示有效,false 表示无效 | true if valid, false if invalid</returns>
|
||||
private static bool IsValidLicenseState(int value)
|
||||
{
|
||||
foreach (var valid in ValidLicenseStates)
|
||||
{
|
||||
if (value == valid) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace XP.Common.License.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// 授权配置实体 | License configuration entity
|
||||
/// </summary>
|
||||
public class LicenseConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 授权模式 | License mode
|
||||
/// </summary>
|
||||
public int LicenseMode { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// 模块ID | Module ID
|
||||
/// </summary>
|
||||
public ushort ModuleId { get; set; } = 4;
|
||||
|
||||
/// <summary>
|
||||
/// 是否使用SMA | Whether to use SMA
|
||||
/// </summary>
|
||||
public bool UseSma { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 授权状态 | License state
|
||||
/// </summary>
|
||||
public int LicenseState { get; set; } = 20;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace XP.Common.License.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 授权模式枚举 | License mode enumeration
|
||||
/// </summary>
|
||||
public enum LicenseMode : int
|
||||
{
|
||||
/// <summary>
|
||||
/// CLMS 正式授权 | CLMS formal authorization
|
||||
/// </summary>
|
||||
Clms = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 临时测试模式(15分钟)| Temporary test mode (15 minutes)
|
||||
/// </summary>
|
||||
TemporaryTest = 885
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace XP.Common.License.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 授权状态枚举 | License state enumeration
|
||||
/// </summary>
|
||||
public enum LicenseState : int
|
||||
{
|
||||
/// <summary>
|
||||
/// 授权成功 | Authorization successful
|
||||
/// </summary>
|
||||
Success = 10,
|
||||
|
||||
/// <summary>
|
||||
/// 授权失败 | Authorization failed
|
||||
/// </summary>
|
||||
Fail = 20
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user