Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6e2e3280a | |||
| c2b525d948 | |||
| cd97cb5d56 | |||
| 04a5ec269f | |||
| 03c7cb58da | |||
| b1871aa9e7 | |||
| de8ce7c4a6 | |||
| bba4cf7939 | |||
| 93c7c1a86b | |||
| 8b07397b5b | |||
| ff4a4cabc8 | |||
| 5adce6c8df | |||
| d6521d4be1 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -2,11 +2,9 @@
|
||||
bin/**
|
||||
build/**
|
||||
|
||||
installer/output/
|
||||
installer/*.wixobj
|
||||
installer/*.msi
|
||||
installer/*.wixpdb
|
||||
*.ps1
|
||||
|
||||
*/.venv
|
||||
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(D330Viewer VERSION 1.0.0 LANGUAGES CXX C)
|
||||
project(Viewer VERSION 1.0.0 LANGUAGES CXX C)
|
||||
|
||||
# 设置C++标准
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
@@ -34,7 +34,7 @@ find_package(Qt6 REQUIRED COMPONENTS
|
||||
)
|
||||
|
||||
# 查找PCL
|
||||
find_package(PCL REQUIRED COMPONENTS common io visualization)
|
||||
find_package(PCL REQUIRED COMPONENTS common io visualization filters)
|
||||
if(PCL_FOUND)
|
||||
include_directories(${PCL_INCLUDE_DIRS})
|
||||
link_directories(${PCL_LIBRARY_DIRS})
|
||||
@@ -101,6 +101,52 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
${RESOURCES}
|
||||
)
|
||||
|
||||
# ==================== 标定文件(cmos0)检查 ====================
|
||||
set(VIEWER_CALIBRATION_DIR "${CMAKE_SOURCE_DIR}/cmos0")
|
||||
set(VIEWER_REQUIRED_CALIB_FILES
|
||||
"coe.txt"
|
||||
"kc.txt"
|
||||
"KK.txt"
|
||||
)
|
||||
|
||||
set(VIEWER_MISSING_CALIB_FILES "")
|
||||
foreach(_calib_file IN LISTS VIEWER_REQUIRED_CALIB_FILES)
|
||||
if(NOT EXISTS "${VIEWER_CALIBRATION_DIR}/${_calib_file}")
|
||||
list(APPEND VIEWER_MISSING_CALIB_FILES "${_calib_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
option(VIEWER_REQUIRE_CALIB_FILES "Fail configure when required cmos0 calibration files are missing" ON)
|
||||
|
||||
if(VIEWER_MISSING_CALIB_FILES)
|
||||
if(VIEWER_REQUIRE_CALIB_FILES)
|
||||
message(FATAL_ERROR
|
||||
"Missing calibration file(s) in ${VIEWER_CALIBRATION_DIR}: ${VIEWER_MISSING_CALIB_FILES}\n"
|
||||
"Please ensure cmos0 contains: ${VIEWER_REQUIRED_CALIB_FILES}"
|
||||
)
|
||||
else()
|
||||
message(WARNING
|
||||
"Missing calibration file(s) in ${VIEWER_CALIBRATION_DIR}: ${VIEWER_MISSING_CALIB_FILES}\n"
|
||||
"Build continues, but runtime or MSI may be incomplete."
|
||||
)
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Calibration files found: ${VIEWER_REQUIRED_CALIB_FILES}")
|
||||
endif()
|
||||
|
||||
# 复制标定文件到运行目录(bin/cmos0)
|
||||
if(EXISTS "${VIEWER_CALIBRATION_DIR}" AND NOT VIEWER_MISSING_CALIB_FILES)
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:${PROJECT_NAME}>/cmos0"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${VIEWER_CALIBRATION_DIR}"
|
||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/cmos0"
|
||||
COMMENT "Copy cmos0 calibration files to runtime directory"
|
||||
)
|
||||
else()
|
||||
message(WARNING "Skip copying cmos0 because required calibration files are missing.")
|
||||
endif()
|
||||
|
||||
# 链接库
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
Qt6::Core
|
||||
@@ -124,37 +170,51 @@ if(WIN32)
|
||||
endif()
|
||||
|
||||
# ==================== 安装配置 ====================
|
||||
# 安装可执行文件
|
||||
# 安装可执行文件到根目录
|
||||
install(TARGETS ${PROJECT_NAME}
|
||||
RUNTIME DESTINATION bin
|
||||
RUNTIME DESTINATION .
|
||||
)
|
||||
|
||||
# 安装所有DLL
|
||||
# 安装所有DLL到根目录
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/bin/
|
||||
DESTINATION bin
|
||||
DESTINATION .
|
||||
FILES_MATCHING PATTERN "*.dll"
|
||||
)
|
||||
|
||||
# 安装Qt平台插件
|
||||
# 安装Qt平台插件到platforms子目录
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/bin/platforms/
|
||||
DESTINATION bin/platforms
|
||||
DESTINATION platforms
|
||||
FILES_MATCHING PATTERN "*.dll"
|
||||
)
|
||||
|
||||
# 安装标定文件目录(用于MSI)
|
||||
if(EXISTS "${VIEWER_CALIBRATION_DIR}" AND NOT VIEWER_MISSING_CALIB_FILES)
|
||||
install(DIRECTORY ${VIEWER_CALIBRATION_DIR}/
|
||||
DESTINATION cmos0
|
||||
FILES_MATCHING
|
||||
PATTERN "*.txt"
|
||||
)
|
||||
endif()
|
||||
|
||||
# ==================== CPack配置 - MSI安装程序 ====================
|
||||
set(CPACK_PACKAGE_NAME "D330Viewer")
|
||||
set(CPACK_PACKAGE_NAME "Viewer")
|
||||
set(CPACK_PACKAGE_VENDOR "Lorenzo Zhao")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "D330M Depth Camera Control System")
|
||||
set(CPACK_PACKAGE_VERSION "0.1.0")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Depth Camera Control System")
|
||||
set(CPACK_PACKAGE_VERSION "0.3.3")
|
||||
set(CPACK_PACKAGE_VERSION_MAJOR "0")
|
||||
set(CPACK_PACKAGE_VERSION_MINOR "1")
|
||||
set(CPACK_PACKAGE_VERSION_PATCH "0")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "D330Viewer")
|
||||
set(CPACK_PACKAGE_VERSION_MINOR "3")
|
||||
set(CPACK_PACKAGE_VERSION_PATCH "3")
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "Viewer")
|
||||
|
||||
# WiX生成器配置(用于MSI)
|
||||
set(CPACK_GENERATOR "WIX")
|
||||
set(CPACK_WIX_UPGRADE_GUID "12345678-1234-1234-1234-123456789012")
|
||||
set(CPACK_WIX_PROGRAM_MENU_FOLDER "D330Viewer")
|
||||
set(CPACK_WIX_UPGRADE_GUID "42365CB0-5840-487F-A2C8-56F9699A9022")
|
||||
set(CPACK_WIX_PROGRAM_MENU_FOLDER "Viewer")
|
||||
set(CPACK_WIX_LICENSE_RTF "${CMAKE_SOURCE_DIR}/LICENSE.rtf")
|
||||
|
||||
# 创建开始菜单和桌面快捷方式
|
||||
set(CPACK_PACKAGE_EXECUTABLES "Viewer" "Viewer")
|
||||
set(CPACK_CREATE_DESKTOP_LINKS "Viewer")
|
||||
|
||||
# 包含CPack模块
|
||||
include(CPack)
|
||||
|
||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
19
LICENSE.rtf
Normal file
19
LICENSE.rtf
Normal file
@@ -0,0 +1,19 @@
|
||||
{\rtf1\ansi\deff0
|
||||
{\fonttbl{\f0 Courier New;}}
|
||||
\f0\fs20
|
||||
GNU GENERAL PUBLIC LICENSE\line
|
||||
Version 3, 29 June 2007\line
|
||||
\line
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\line
|
||||
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.\line
|
||||
\line
|
||||
Preamble\line
|
||||
\line
|
||||
The GNU General Public License is a free, copyleft license for software and other kinds of works.\line
|
||||
\line
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\line
|
||||
\line
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\line
|
||||
\line
|
||||
You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.\line
|
||||
}
|
||||
91
README.md
91
README.md
@@ -109,25 +109,36 @@ candle.exe -?
|
||||
|
||||
**生成MSI安装程序**:
|
||||
|
||||
**方式1:使用CPack(推荐)**
|
||||
```bash
|
||||
# 编译完成后,在build目录运行
|
||||
cd build
|
||||
cpack -G WIX -C Release
|
||||
```
|
||||
生成的安装程序:`build/D330Viewer-0.1.0-win64.msi`
|
||||
|
||||
**方式2:使用WiX Toolset手动构建**
|
||||
```bash
|
||||
build_installer.bat
|
||||
```
|
||||
生成的安装程序:`installer/output/D330Viewer.msi`
|
||||
生成的安装程序:`build/D330Viewer-0.1.0-win64.msi`
|
||||
|
||||
**MSI安装程序包含**:
|
||||
- D330Viewer.exe(主程序)
|
||||
- 所有必需的DLL(37个)
|
||||
- 所有必需的DLL
|
||||
- Qt平台插件(platforms目录)
|
||||
- 开始菜单快捷方式
|
||||
- 桌面快捷方式
|
||||
- 卸载程序快捷方式(开始菜单)
|
||||
|
||||
**安装目录结构**:
|
||||
```
|
||||
C:\Program Files\D330Viewer\
|
||||
├── D330Viewer.exe # 主程序
|
||||
├── *.dll # 所有依赖库
|
||||
└── platforms\ # Qt平台插件
|
||||
└── qwindows.dll
|
||||
```
|
||||
|
||||
**快捷方式**:
|
||||
- 桌面:D330Viewer(卸载时自动删除)
|
||||
- 开始菜单:D330Viewer 文件夹
|
||||
- D330Viewer(启动程序)
|
||||
- 卸载 D330Viewer(卸载程序)
|
||||
|
||||
## 功能特性
|
||||
|
||||
@@ -137,10 +148,12 @@ build_installer.bat
|
||||
- ✅ UDP网络通信(GVSP协议解析)
|
||||
- ✅ 自动设备扫描和发现
|
||||
- ✅ 相机连接管理(连接/断开)
|
||||
- ✅ 命令发送(START/STOP/ONCE)
|
||||
- ✅ 命令发送(START/STOP)
|
||||
- ✅ 下位机动态切换IP,可随时切换上位机
|
||||
|
||||
#### 可视化
|
||||
- ✅ 实时深度图显示(OpenCV,垂直翻转校正)
|
||||
- ✅ 实时左红外、右红外相机图像显示
|
||||
- ✅ 实时RGB相机图像显示
|
||||
- ✅ 实时3D点云显示(自定义OpenGL渲染)
|
||||
- ✅ 正交投影视角(平面视图)
|
||||
- ✅ 交互式点云操作:
|
||||
@@ -150,24 +163,69 @@ build_installer.bat
|
||||
|
||||
#### 用户界面
|
||||
- ✅ Qt6 GUI主窗口(三栏布局)
|
||||
- ✅ 相机控制面板(START/STOP/ONCE按钮)
|
||||
- ✅ 相机控制面板(START/STOP按钮)
|
||||
- ✅ 曝光时间调节(滑块,范围460-100000μs)
|
||||
- ✅ 网络配置(IP地址、端口设置)
|
||||
- ✅ 连接状态指示
|
||||
- ✅ 配置持久化(QSettings)
|
||||
- ✅ 点云颜色映射(深度着色)
|
||||
- ✅ 多视角预设(正视、侧视、俯视)
|
||||
|
||||
### 🚧 当前开发计划
|
||||
|
||||
根据需求文档和用户反馈,后续待添加功能如下:
|
||||
|
||||
- 录制功能(连续保存多帧)
|
||||
- 点云颜色映射(深度着色)
|
||||
- 点云滤波选项(降噪、平滑)
|
||||
- 测量工具(距离、角度测量)
|
||||
- 多视角预设(正视、侧视、俯视)
|
||||
- 性能监控(CPU/GPU使用率、内存使用)
|
||||
- 其他相机参数调节(增益、白平衡等)
|
||||
|
||||
## 点云去噪原理与参数说明
|
||||
|
||||
### 去噪处理流程(当前版本)
|
||||
|
||||
当前点云去噪不是单一滤波器,而是多阶段组合策略,目标是在保留主体结构的同时抑制放射状无效点和外围杂点。
|
||||
|
||||
1. 有效点预筛:去掉非有限值和 `z<=0` 的点,得到基础有效掩码。
|
||||
2. 中心ROI深度门控:基于中心区域中位深度自适应裁剪深度窗口,先去掉明显离群深度。
|
||||
3. 邻域一致性筛选:统计每个点在局部窗口内“深度相近邻居”的数量,邻域支持不足的点剔除。
|
||||
4. 形态学轻清理:移除局部孤立残点,减少毛刺。
|
||||
5. 近距离尾部裁剪:对低深度尾部进行比例裁剪,抑制中心放射状噪点。
|
||||
6. 连通簇筛选:按面积、深度一致性和中心重叠等条件保留主簇及相关簇,抑制周边散簇。
|
||||
7. 最终细枝清理:对近距离且邻居不足的细枝点做额外抑制。
|
||||
8. 时序稳定:对关键阈值做帧间平滑和限跳,减少块状点云“时有时无”的闪烁。
|
||||
|
||||
### 三个参数的作用与范围
|
||||
|
||||
参数都在“曝光与拍照 -> 拍照参数 -> 点云去噪参数”中,实时生效。
|
||||
|
||||
1. 邻域支持阈值
|
||||
- 范围:`3 ~ 12`
|
||||
- 含义:一个点要保留,局部邻域内至少需要多少个深度相近邻居。
|
||||
- 调大:噪点更少,但边缘和细小结构更容易被吃掉。
|
||||
- 调小:细节更多,但散点噪声会增加。
|
||||
|
||||
2. 射线裁剪强度 (‰)
|
||||
- 范围:`5 ~ 50`
|
||||
- 含义:近距离低深度尾部的裁剪比例(千分比)。
|
||||
- 调大:中心放射状噪点减少更明显,但近距离真实细节可能减少。
|
||||
- 调小:近距离细节保留更多,但放射状点可能增多。
|
||||
|
||||
3. 周边抑制带宽 (‰)
|
||||
- 范围:`40 ~ 180`
|
||||
- 含义:控制连通簇保留深度带宽、回补范围和近距离毛刺门限。
|
||||
- 调小:抑制更激进,周边杂点更少,但主体可能偏“硬”、易丢块。
|
||||
- 调大:主体与细节更完整,但外围杂点回升概率更高。
|
||||
|
||||
### 推荐起始参数
|
||||
|
||||
用于室内桌椅等常见场景,可先从以下值起步,再按效果微调:
|
||||
|
||||
- 邻域支持阈值:`8 ~ 10`
|
||||
- 射线裁剪强度:`12 ~ 18`
|
||||
- 周边抑制带宽:`90 ~ 130`
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
@@ -190,12 +248,10 @@ d330viewer/
|
||||
│ └── core/ # 核心功能头文件
|
||||
├── bin/ # 可执行文件和DLL
|
||||
│ ├── D330Viewer.exe # 主程序
|
||||
│ └── *.dll # 依赖库(37个)
|
||||
│ └── *.dll # 依赖库
|
||||
├── build/ # CMake构建目录
|
||||
├── installer/ # MSI安装程序配置
|
||||
├── CMakeLists.txt # CMake配置
|
||||
├── configure.bat # 快速配置脚本
|
||||
├── build_installer.bat # MSI安装程序构建脚本
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
@@ -249,7 +305,6 @@ d330viewer/
|
||||
|
||||
5. **停止采集**
|
||||
- 点击"停止"按钮停止采集
|
||||
- 点击"单次"按钮采集单帧
|
||||
|
||||
### 点云交互操作
|
||||
|
||||
@@ -274,4 +329,4 @@ d330viewer/
|
||||
- 使用Qt6信号槽机制进行模块间通信
|
||||
- OpenCL kernel代码内联在C++源文件中
|
||||
- 配置使用QSettings持久化
|
||||
- 日志输出到 `bin/d330viewer.log`
|
||||
- 日志输出到 `%LOCALAPPDATA%/Viewer/Viewer/viewer.log`(例如 `C:/Users/<用户名>/AppData/Local/Viewer/Viewer/viewer.log`)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
@echo off
|
||||
echo ========================================
|
||||
echo D330Viewer MSI Installer Builder
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 检查WiX Toolset是否安装
|
||||
where candle.exe >nul 2>&1
|
||||
if %ERRORLEVEL% NEQ 0 (
|
||||
echo ERROR: WiX Toolset not found!
|
||||
echo Please install WiX Toolset from: https://wixtoolset.org/
|
||||
echo After installation, add WiX bin directory to PATH
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [1/5] Checking build output...
|
||||
if not exist "bin\D330Viewer.exe" (
|
||||
echo ERROR: D330Viewer.exe not found in bin directory
|
||||
echo Please build the project first: cmake --build build --config Release
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/5] Creating installer directory...
|
||||
if not exist "installer\output" mkdir installer\output
|
||||
|
||||
echo [3/5] Harvesting DLL files...
|
||||
REM 使用heat.exe自动收集bin目录下的所有文件
|
||||
heat.exe dir bin -cg BinFiles -gg -scom -sreg -sfrag -srd -dr INSTALLFOLDER -var var.BinDir -out installer\BinFiles.wxs
|
||||
|
||||
echo [4/5] Compiling WiX source...
|
||||
candle.exe -dBinDir=bin installer\UpperControl.wxs installer\BinFiles.wxs -out installer\output\
|
||||
|
||||
echo [5/5] Linking MSI package...
|
||||
light.exe -ext WixUIExtension installer\output\UpperControl.wixobj installer\output\BinFiles.wixobj -out installer\output\D330Viewer.msi
|
||||
|
||||
if exist "installer\output\D330Viewer.msi" (
|
||||
echo.
|
||||
echo ========================================
|
||||
echo SUCCESS! MSI installer created:
|
||||
echo installer\output\D330Viewer.msi
|
||||
echo ========================================
|
||||
) else (
|
||||
echo.
|
||||
echo ERROR: Failed to create MSI installer
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
pause
|
||||
3
cmos0/KK.txt
Normal file
3
cmos0/KK.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
1.4328957e+03 0.0000000e+00 6.3751170e+02
|
||||
0.0000000e+00 1.4326590e+03 5.2187200e+02
|
||||
0.0000000e+00 0.0000000e+00 1.0000000e+00
|
||||
1224
cmos0/coe.txt
Normal file
1224
cmos0/coe.txt
Normal file
File diff suppressed because it is too large
Load Diff
5
cmos0/kc.txt
Normal file
5
cmos0/kc.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
-1.2009005e-01
|
||||
1.1928703e-01
|
||||
9.6197371e-05
|
||||
-1.4896083e-04
|
||||
0.0000000e+00
|
||||
@@ -4,7 +4,7 @@ REM CMake配置脚本 - Windows版本
|
||||
REM 请根据实际安装路径修改以下变量
|
||||
|
||||
echo ========================================
|
||||
echo D330Viewer CMake配置脚本
|
||||
echo Viewer CMake配置脚本
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
@@ -47,7 +47,7 @@ if %ERRORLEVEL% EQU 0 (
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 下一步:
|
||||
echo 1. 打开 build\D330Viewer.sln 使用Visual Studio编译
|
||||
echo 1. 打开 build\Viewer.sln 使用Visual Studio编译
|
||||
echo 2. 或运行: cmake --build build --config Release
|
||||
echo.
|
||||
) else (
|
||||
|
||||
@@ -4,31 +4,45 @@
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <QImage>
|
||||
#include <QAtomicInt>
|
||||
#include <cstdint>
|
||||
|
||||
// GVSP packet types
|
||||
// GVSP packet types (GigE Vision 2.1 standard)
|
||||
#define GVSP_LEADER_PACKET 0x01
|
||||
#define GVSP_PAYLOAD_PACKET 0x02
|
||||
#define GVSP_TRAILER_PACKET 0x03
|
||||
#define GVSP_TRAILER_PACKET 0x02
|
||||
#define GVSP_PAYLOAD_PACKET 0x03
|
||||
|
||||
// Payload types
|
||||
#define PAYLOAD_TYPE_IMAGE 0x0001
|
||||
#define PAYLOAD_TYPE_BINARY 0x0003
|
||||
#define PAYLOAD_TYPE_IMAGE 0x0001
|
||||
#define PAYLOAD_TYPE_BINARY 0x0003
|
||||
#define PAYLOAD_TYPE_POINTCLOUD 0x8000 // Vendor-specific for point cloud data
|
||||
|
||||
// Image format
|
||||
#define PIXEL_FORMAT_12BIT_GRAY 0x010C0001
|
||||
#define PIXEL_FORMAT_MONO16 0x01100005 // Mono16 format (legacy)
|
||||
#define PIXEL_FORMAT_MONO16_LEFT 0x01100006 // Mono16 format for left IR camera
|
||||
#define PIXEL_FORMAT_MONO16_RIGHT 0x01100007 // Mono16 format for right IR camera
|
||||
#define PIXEL_FORMAT_MONO8_LEFT 0x01080006 // Mono8 format for left IR camera (downsampled)
|
||||
#define PIXEL_FORMAT_MONO8_RIGHT 0x01080007 // Mono8 format for right IR camera (downsampled)
|
||||
#define PIXEL_FORMAT_MJPEG 0x02180001 // MJPEG format for RGB camera
|
||||
|
||||
// Image dimensions
|
||||
#define IMAGE_WIDTH 1224
|
||||
#define IMAGE_HEIGHT 1024
|
||||
#define IR_DISPLAY_WIDTH 612 // Downsampled IR display width
|
||||
#define IR_DISPLAY_HEIGHT 512 // Downsampled IR display height
|
||||
#define RGB_WIDTH 1920
|
||||
#define RGB_HEIGHT 1080
|
||||
|
||||
#pragma pack(push, 1)
|
||||
|
||||
// GVSP packet header
|
||||
// GVSP packet header (GigE Vision 2.1 standard - 8 bytes)
|
||||
struct GVSPPacketHeader {
|
||||
uint16_t status;
|
||||
uint16_t block_id;
|
||||
uint32_t packet_fmt_id;
|
||||
uint16_t status; // Status flags
|
||||
uint16_t block_id; // Block ID (frame number)
|
||||
uint8_t packet_format; // Packet type: 0x01=Leader, 0x02=Trailer, 0x03=Payload
|
||||
uint8_t reserved; // Reserved byte
|
||||
uint16_t packet_id; // Packet ID within block
|
||||
};
|
||||
|
||||
// Image data leader
|
||||
@@ -71,6 +85,22 @@ struct GVSPBinaryDataTrailer {
|
||||
uint32_t checksum;
|
||||
};
|
||||
|
||||
// Point cloud data leader (vendor-specific, payload_type = 0x8000)
|
||||
struct GVSPPointCloudDataLeader {
|
||||
uint16_t reserved;
|
||||
uint16_t payload_type; // 0x8000
|
||||
uint32_t timestamp_high;
|
||||
uint32_t timestamp_low;
|
||||
uint32_t data_size; // Total size of point cloud data
|
||||
};
|
||||
|
||||
// Point cloud data trailer
|
||||
struct GVSPPointCloudDataTrailer {
|
||||
uint32_t reserved;
|
||||
uint16_t payload_type; // 0x8000
|
||||
uint32_t checksum;
|
||||
};
|
||||
|
||||
#pragma pack(pop)
|
||||
|
||||
class GVSPParser : public QObject
|
||||
@@ -85,37 +115,57 @@ public:
|
||||
void reset();
|
||||
|
||||
signals:
|
||||
void imageReceived(const QImage &image, uint32_t blockId);
|
||||
void depthDataReceived(const QByteArray &depthData, uint32_t blockId);
|
||||
void leftImageReceived(const QByteArray &jpegData, uint32_t blockId); // 左红外图像(JPEG)
|
||||
void rightImageReceived(const QByteArray &jpegData, uint32_t blockId); // 右红外图像(JPEG)
|
||||
void rgbImageReceived(const QByteArray &jpegData, uint32_t blockId); // RGB图像(MJPEG)
|
||||
void pointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId);
|
||||
void parseError(const QString &error);
|
||||
|
||||
// 兼容旧代码
|
||||
void imageReceived(const QImage &image, uint32_t blockId);
|
||||
void depthDataReceived(const QByteArray &depthData, uint32_t blockId);
|
||||
|
||||
private:
|
||||
// 每种数据流的独立接收状态
|
||||
struct StreamState {
|
||||
bool isReceiving;
|
||||
uint32_t blockId;
|
||||
QByteArray dataBuffer;
|
||||
size_t expectedSize;
|
||||
size_t receivedSize;
|
||||
int packetCount;
|
||||
|
||||
// 图像特有信息
|
||||
uint32_t imageWidth;
|
||||
uint32_t imageHeight;
|
||||
uint32_t pixelFormat;
|
||||
|
||||
StreamState() : isReceiving(false), blockId(0), expectedSize(0),
|
||||
receivedSize(0), packetCount(0), imageWidth(0),
|
||||
imageHeight(0), pixelFormat(0) {}
|
||||
};
|
||||
|
||||
void handleLeaderPacket(const uint8_t *data, size_t size);
|
||||
void handlePayloadPacket(const uint8_t *data, size_t size);
|
||||
void handleTrailerPacket(const uint8_t *data, size_t size);
|
||||
|
||||
void processImageData();
|
||||
void processDepthData();
|
||||
void processImageData(StreamState *state);
|
||||
void processDepthData(StreamState *state);
|
||||
void processPointCloudData(StreamState *state);
|
||||
|
||||
private:
|
||||
// Reception state
|
||||
bool m_isReceiving;
|
||||
int m_dataType; // 0=unknown, 1=image, 3=depth
|
||||
uint32_t m_currentBlockId;
|
||||
|
||||
// Data buffer
|
||||
QByteArray m_dataBuffer;
|
||||
size_t m_expectedSize;
|
||||
size_t m_receivedSize;
|
||||
|
||||
// Image info
|
||||
uint32_t m_imageWidth;
|
||||
uint32_t m_imageHeight;
|
||||
uint32_t m_pixelFormat;
|
||||
// 为每种数据类型维护独立的状态
|
||||
StreamState m_leftIRState; // 左红外
|
||||
StreamState m_rightIRState; // 右红外
|
||||
StreamState m_rgbState; // RGB
|
||||
StreamState m_depthState; // 深度数据
|
||||
StreamState m_pointCloudState; // 点云数据
|
||||
|
||||
// Statistics
|
||||
uint32_t m_lastBlockId;
|
||||
int m_packetCount;
|
||||
int m_imageSequence;
|
||||
|
||||
// Async processing control
|
||||
QAtomicInt m_imageProcessingCount;
|
||||
};
|
||||
|
||||
#endif // GVSPPARSER_H
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#ifndef POINTCLOUDPROCESSOR_H
|
||||
#ifndef POINTCLOUDPROCESSOR_H
|
||||
#define POINTCLOUDPROCESSOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QByteArray>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <pcl/point_cloud.h>
|
||||
#include <pcl/point_types.h>
|
||||
#include <CL/cl.h>
|
||||
@@ -15,41 +17,38 @@ public:
|
||||
explicit PointCloudProcessor(QObject *parent = nullptr);
|
||||
~PointCloudProcessor();
|
||||
|
||||
// 初始化OpenCL
|
||||
bool initializeOpenCL();
|
||||
|
||||
// 设置相机内参
|
||||
void setCameraIntrinsics(float fx, float fy, float cx, float cy);
|
||||
|
||||
// 设置Z缩放因子
|
||||
void setZScaleFactor(float scale);
|
||||
|
||||
// 将深度数据转换为点云(使用OpenCL GPU加速)
|
||||
void processDepthData(const QByteArray &depthData, uint32_t blockId);
|
||||
void processPointCloudData(const QByteArray &cloudData, uint32_t blockId);
|
||||
|
||||
void setDenoiseEnabled(bool enabled);
|
||||
void setDenoiseNeighborSupport(int minNeighbors);
|
||||
void setDenoiseLowTailPermille(int permille);
|
||||
void setDenoiseDepthBandPermille(int permille);
|
||||
|
||||
signals:
|
||||
void pointCloudReady(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, uint32_t blockId);
|
||||
void errorOccurred(const QString &error);
|
||||
|
||||
private:
|
||||
// 清理OpenCL资源
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr applyDenoise(const pcl::PointCloud<pcl::PointXYZ>::Ptr &input);
|
||||
void loadLowerCalibration();
|
||||
void cleanupOpenCL();
|
||||
|
||||
// 相机内参
|
||||
float m_fx;
|
||||
float m_fy;
|
||||
float m_cx;
|
||||
float m_cy;
|
||||
|
||||
// Z缩放因子
|
||||
float m_zScale;
|
||||
|
||||
// 图像尺寸
|
||||
int m_imageWidth;
|
||||
int m_imageHeight;
|
||||
int m_totalPoints;
|
||||
|
||||
// OpenCL资源
|
||||
cl_platform_id m_platform;
|
||||
cl_device_id m_device;
|
||||
cl_context m_context;
|
||||
@@ -59,6 +58,30 @@ private:
|
||||
cl_mem m_depthBuffer;
|
||||
cl_mem m_xyzBuffer;
|
||||
bool m_clInitialized;
|
||||
|
||||
std::atomic_bool m_denoiseEnabled;
|
||||
float m_voxelLeafSize;
|
||||
std::atomic_int m_denoiseNeighborSupport;
|
||||
std::atomic_int m_denoiseLowTailPermille;
|
||||
std::atomic_int m_denoiseDepthBandPermille;
|
||||
|
||||
// Calibration params aligned with lower-machine model
|
||||
float m_k1;
|
||||
float m_k2;
|
||||
float m_p1;
|
||||
float m_p2;
|
||||
float m_p5;
|
||||
float m_p6;
|
||||
float m_p7;
|
||||
float m_p8;
|
||||
bool m_hasLowerCalibration;
|
||||
|
||||
// Temporal stabilizers for denoise to reduce frame-to-frame flicker.
|
||||
std::mutex m_denoiseStateMutex;
|
||||
bool m_hasAnchorMeanZ;
|
||||
float m_anchorMeanZFiltered;
|
||||
bool m_hasLowCutZ;
|
||||
float m_lowCutZFiltered;
|
||||
};
|
||||
|
||||
#endif // POINTCLOUDPROCESSOR_H
|
||||
|
||||
@@ -23,6 +23,9 @@ public:
|
||||
~PointCloudGLWidget();
|
||||
|
||||
void updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud);
|
||||
void setColorMode(bool enabled) { m_colorMode = enabled ? 1 : 0; update(); }
|
||||
bool colorMode() const { return m_colorMode != 0; }
|
||||
void resetView(); // 重置视角到初始状态
|
||||
|
||||
protected:
|
||||
void initializeGL() override;
|
||||
@@ -48,21 +51,31 @@ private:
|
||||
// 点云数据
|
||||
std::vector<float> m_vertices;
|
||||
int m_pointCount;
|
||||
float m_minZ, m_maxZ; // 深度范围(用于着色)
|
||||
|
||||
// 相机参数
|
||||
QMatrix4x4 m_projection;
|
||||
QMatrix4x4 m_view;
|
||||
QMatrix4x4 m_model;
|
||||
|
||||
float m_orthoSize; // 正交投影视野大小(控制缩放)
|
||||
float m_fov; // 透视投影视场角
|
||||
float m_rotationX; // X轴旋转角度
|
||||
float m_rotationY; // Y轴旋转角度
|
||||
QVector3D m_translation; // 平移
|
||||
QVector3D m_cloudCenter; // 点云中心
|
||||
float m_viewDistance; // 观察距离
|
||||
QVector3D m_panOffset; // 用户平移偏移
|
||||
float m_zoom; // 缩放因子
|
||||
|
||||
// 鼠标交互状态
|
||||
QPoint m_lastMousePos;
|
||||
bool m_leftButtonPressed;
|
||||
bool m_rightButtonPressed;
|
||||
|
||||
// 首帧标志(只在首帧时自动居中)
|
||||
bool m_firstFrame;
|
||||
|
||||
// 颜色模式(0=黑白,1=彩色)
|
||||
int m_colorMode;
|
||||
};
|
||||
|
||||
#endif // POINTCLOUDGLWIDGET_H
|
||||
|
||||
@@ -19,6 +19,10 @@ public:
|
||||
// 更新点云显示
|
||||
void updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud);
|
||||
|
||||
// 颜色模式控制
|
||||
void setColorMode(bool enabled);
|
||||
bool colorMode() const;
|
||||
|
||||
private:
|
||||
QLabel *m_statusLabel;
|
||||
PointCloudGLWidget *m_glWidget;
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Fragment>
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="cmpC3C56006B21D7A9B1C3D970559A58022" Guid="{CC08CF56-C517-49D0-ABCF-9F873190FB5D}">
|
||||
<File Id="filA38101C31D3FE72B90DABBD659FA3749" KeyPath="yes" Source="$(var.BinDir)\CLAllSerial_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp94AE231A385A67D66B846FBD16D60636" Guid="{AF528182-E790-4F99-865F-96A8BAC63C64}">
|
||||
<File Id="fil24783C0C3176434BE2433E90A26639B5" KeyPath="yes" Source="$(var.BinDir)\CLProtocol_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp5BBD8A0A8D3848D3C63356E9ADDC3DAB" Guid="{49379FC3-4074-47AC-912F-9AF48F097D4A}">
|
||||
<File Id="fil96040E48A1CEC0DD77E92374F304C405" KeyPath="yes" Source="$(var.BinDir)\clsercom.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp41DC9741013CA4AD62A4A5FF421F08BC" Guid="{2D62ADE6-D030-4233-9A5B-140D90B3D415}">
|
||||
<File Id="fil84B847A9F8DABE6B95AF875695C9E6D1" KeyPath="yes" Source="$(var.BinDir)\d3dcompiler_47.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp5CADE23B444DBEA082D543627C541D33" Guid="{DA896890-8E2B-4061-86F6-321716A99339}">
|
||||
<File Id="fil8F54BE958899A2BFF99AC72F9A899AF9" KeyPath="yes" Source="$(var.BinDir)\DkamSDK.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpFC9F1FCE7DF02EBFFBBB05A2D04DF5C9" Guid="{621784F4-1F1E-445B-B77F-4E22E28EA9AB}">
|
||||
<File Id="fil5B0CA26742B9001E819A6C24AB1DFE4E" KeyPath="yes" Source="$(var.BinDir)\DkamSDK_CSharp.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp0A946DD3BC4835FBF8A86E6F14AFF450" Guid="{438AC89E-097C-4091-BA8F-C3292A615684}">
|
||||
<File Id="filC769392EA3BDC8D6035070F861669864" KeyPath="yes" Source="$(var.BinDir)\DkamSDK_CSharp_Namespace.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp304DB2D25065C3D5B1F99012C3F097B3" Guid="{FD24F279-1A97-4F5F-893B-A8DBC387F094}">
|
||||
<File Id="fil47D910A70309D29213F27AFB1B0B9421" KeyPath="yes" Source="$(var.BinDir)\FirmwareUpdate_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpA1FAB8CA5E7B238E225525086D59754B" Guid="{D61BBE30-31BB-44BA-9AAC-62F888F0FA0C}">
|
||||
<File Id="filEBA00F3DBC3F68DCF9B825874F6738CE" KeyPath="yes" Source="$(var.BinDir)\GCBase_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp0D1074CD4A4DC67B6653DCF74385DF0E" Guid="{94C5E933-E14B-4AF4-B172-78663F76989A}">
|
||||
<File Id="fil30A6FD8B9DD3FC13C5C579A4DBBDBC92" KeyPath="yes" Source="$(var.BinDir)\GenApi_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp9A9D29623257FA41663F84EAA2612688" Guid="{CEA04915-E4F8-4531-AE8D-41CAFE22C6D8}">
|
||||
<File Id="filB94200E89E3B121E295845B256282799" KeyPath="yes" Source="$(var.BinDir)\GenCP_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp45C131D18AAA1AE3EF9B48BCFCC28F6D" Guid="{F96A1BAD-8C80-4802-B549-481D5C246567}">
|
||||
<File Id="fil8354EA4C33A6C488882967BAC14C59EA" KeyPath="yes" Source="$(var.BinDir)\log4cpp_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpA829C4573EB498556AC47A36290EBF20" Guid="{0016F5BD-D8CE-40D1-A8BC-0D01F9D6AD49}">
|
||||
<File Id="fil4D74A6B62C6930C4CE69FA293FA47754" KeyPath="yes" Source="$(var.BinDir)\Log_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp61CA0314C827E13940ED8588B8A8BA13" Guid="{574DB953-993C-46E7-94ED-CC770194A6B9}">
|
||||
<File Id="filBB5A2C2AEC26359B2C6F23420800AE9D" KeyPath="yes" Source="$(var.BinDir)\MathParser_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpEDDED65BD9A61BB47433821AA149DC39" Guid="{9AAE661B-0960-4D6A-B64E-B3E838DADD4E}">
|
||||
<File Id="fil9EAA71401D5302036DABA47B8EF6B434" KeyPath="yes" Source="$(var.BinDir)\NodeMapData_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpDAC39AC2BB8715FBE58F904CC2E9F698" Guid="{CF6BC55C-FF3E-421D-A7BF-98435449455E}">
|
||||
<File Id="fil3940E64D4079916BAF62ED6C54607F0E" KeyPath="yes" Source="$(var.BinDir)\opencv_world4130.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp316B7EADCCFA5F980015637E294FFC4B" Guid="{F99C0165-0728-4384-A782-7659F766FA25}">
|
||||
<File Id="fil07CE2ABE9F28D446757A019F07E1898F" KeyPath="yes" Source="$(var.BinDir)\opengl32sw.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp0C61783999259F0182CE807EF9128FF4" Guid="{184991A1-36A0-4830-B9DD-A58E07FBC1A1}">
|
||||
<File Id="fil449440E65BE6F95AC788ED910D6D3FB4" KeyPath="yes" Source="$(var.BinDir)\pcl_common.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpB9B2B93E50693FCD82547A4EEDF59C47" Guid="{60CA5622-9531-43B7-BD6B-C82625D7B805}">
|
||||
<File Id="filC346A45D1A8B2E29F1558CA7C1156D48" KeyPath="yes" Source="$(var.BinDir)\pcl_filters.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpE0368B606CE94148F4CA5F032A18B8AA" Guid="{F8F62E47-F16E-4FC1-BA4C-2A786A73A80B}">
|
||||
<File Id="filE14680FDD24600E0C7BC68717F06DB13" KeyPath="yes" Source="$(var.BinDir)\pcl_io.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpA478CC827F5FA0CCF27AAD818849CBD7" Guid="{0C9073A4-A6FA-4BAB-9733-8A704E8ECF01}">
|
||||
<File Id="fil3B0E0708353E72AE7456B38B7F5BC970" KeyPath="yes" Source="$(var.BinDir)\pcl_io_ply.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp5878BDE96D855AFAF94266F3B9677E88" Guid="{279F0111-C41A-4501-BA4B-A750DD69B0F4}">
|
||||
<File Id="fil7470D204801B87A04BF47FD21B3750BF" KeyPath="yes" Source="$(var.BinDir)\pcl_kdtree.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp0759BB73FE6272908152EAACE25509E2" Guid="{62BD1558-338F-46F7-9B67-4C7F18FD443C}">
|
||||
<File Id="filCE419FFD12F6A6C0A0ED57B48BC8D9D8" KeyPath="yes" Source="$(var.BinDir)\pcl_octree.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp6B64D18EAB4E2AA54759D9616AB99FC6" Guid="{5972BA2F-7BED-4D8D-8C73-8E6131DAD884}">
|
||||
<File Id="fil82FA0626A335B90C9078B5F4C52872B3" KeyPath="yes" Source="$(var.BinDir)\pcl_outofcore.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpE6ECB1DBD11FF2337C376EACE8AC5BC6" Guid="{9CC56B62-4A75-4583-B94B-09DE77D057D3}">
|
||||
<File Id="fil750E9427DCF40DCEB7349243CE6BCCFE" KeyPath="yes" Source="$(var.BinDir)\pcl_sample_consensus.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpE6D8206385B0C897A99FF044F618C63E" Guid="{5F5D35F9-F55E-4D38-8AD2-991EA8C65DB5}">
|
||||
<File Id="fil360C6C1D4A2DB31173843F5596F2C54F" KeyPath="yes" Source="$(var.BinDir)\pcl_search.dll" />
|
||||
</Component>
|
||||
<Component Id="cmpD5BFC0D57560599B60E42E5501314FEB" Guid="{DB78A410-C481-4E70-B94C-02FCB980E63E}">
|
||||
<File Id="fil263B8621AC2C78E67B986C7BC1E576CD" KeyPath="yes" Source="$(var.BinDir)\pcl_segmentation.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp82FF9A9C17F8D75F054D5CDDB8768C5C" Guid="{38967235-8639-4C79-9E7C-724504137A1E}">
|
||||
<File Id="filD3270351B41213BA8E4318BBB4E5F2E9" KeyPath="yes" Source="$(var.BinDir)\pcl_surface.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp396E4B41C7A33BE56F778473828172F7" Guid="{B219C434-6BE7-45F0-8831-22D44291DF41}">
|
||||
<File Id="filCE07DA5E17AE0C265C2A1738147B6059" KeyPath="yes" Source="$(var.BinDir)\Qt6Concurrent.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp02DF6A9A3BF862D35391F764FE67153F" Guid="{0244645B-D7B2-42C9-8D61-09EBDE868F3D}">
|
||||
<File Id="fil494DCCE77F31DE27CD7C03A13FD938B0" KeyPath="yes" Source="$(var.BinDir)\Qt6Core.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp701A262B1CE5139B2E57DF21A5AA3B94" Guid="{53E7B97E-8BD1-4EFB-B232-D80A305CE61E}">
|
||||
<File Id="filD9F54CB97035337EAF318E4E55DD2F5F" KeyPath="yes" Source="$(var.BinDir)\Qt6Gui.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp1EAE9F3A379CF532AF1EBC883CDCD1D2" Guid="{5267D680-F1AD-4648-84D2-F68A45111EC0}">
|
||||
<File Id="fil15AC8A9B3B6A6F1FE6BBDDAE2042D5D0" KeyPath="yes" Source="$(var.BinDir)\Qt6Network.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp6C95449DAD236B0C9989FA43DD56CA85" Guid="{9BFC4239-AAD9-4D40-9058-FB26E9E2F8A9}">
|
||||
<File Id="fil93AD72205F2074362FE1D69894AF2E4E" KeyPath="yes" Source="$(var.BinDir)\Qt6OpenGL.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp11109A2A7F5FEC28295B469293762519" Guid="{B83D1B28-0FB2-4BDC-BE57-0943E296AAC0}">
|
||||
<File Id="fil9BB89E2877718991879476005F911AF2" KeyPath="yes" Source="$(var.BinDir)\Qt6OpenGLWidgets.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp5B0A9912E5069950C30FC5FF8EAA16D4" Guid="{70A602C1-1808-4DC5-9822-ECF2A321A750}">
|
||||
<File Id="fil9F3707482AFAB8A22C7B5C23D8C7CE69" KeyPath="yes" Source="$(var.BinDir)\Qt6Widgets.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp1B37E98B173B46E1E5315143EF8D7728" Guid="{89E23A4E-DACC-4A16-AC16-98C696218F10}">
|
||||
<File Id="fil27A091E66DC753FB25B8F53726B5D53D" KeyPath="yes" Source="$(var.BinDir)\Qt6Xml.dll" />
|
||||
</Component>
|
||||
<Component Id="cmp2F414B6DDC2ADD0824C2DDE58E9102FA" Guid="{B50C2D2D-629F-4688-8D16-271CB92626B7}">
|
||||
<File Id="filB3ED52B8FA49C0BD845D3E52B9F7F279" KeyPath="yes" Source="$(var.BinDir)\uppercontrol.log" />
|
||||
</Component>
|
||||
<Component Id="cmp3FB3F428740E959925E49E3155B50A4B" Guid="{D5A06D7C-FD95-456E-861D-EEC0ED327631}">
|
||||
<File Id="filF458411F7CE6D0DFCA440791D37E30EA" KeyPath="yes" Source="$(var.BinDir)\UpperControlGUI.exe" />
|
||||
</Component>
|
||||
<Component Id="cmpACC01C1F599CA1C44DC57D4D6199427D" Guid="{DA47AC68-F657-4FDD-90B9-5B3E82088F21}">
|
||||
<File Id="filBC2C4C6DBCF2DD007E8E416D1CFFBDC7" KeyPath="yes" Source="$(var.BinDir)\XmlParser_MD_VC120_v3_1.dll" />
|
||||
</Component>
|
||||
<Directory Id="dir908DFC228501FBE723FC009E659CB04F" Name="platforms">
|
||||
<Component Id="cmp3E80143A725EE7380FA78AA9E8BD2283" Guid="{04575A8F-DF0B-4345-BBCE-CB9B5BCD7AF6}">
|
||||
<File Id="fil702C83C4DEF82CE8980ED1100FE61CE2" KeyPath="yes" Source="$(var.BinDir)\platforms\qwindows.dll" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</DirectoryRef>
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
<ComponentGroup Id="BinFiles">
|
||||
<ComponentRef Id="cmpC3C56006B21D7A9B1C3D970559A58022" />
|
||||
<ComponentRef Id="cmp94AE231A385A67D66B846FBD16D60636" />
|
||||
<ComponentRef Id="cmp5BBD8A0A8D3848D3C63356E9ADDC3DAB" />
|
||||
<ComponentRef Id="cmp41DC9741013CA4AD62A4A5FF421F08BC" />
|
||||
<ComponentRef Id="cmp5CADE23B444DBEA082D543627C541D33" />
|
||||
<ComponentRef Id="cmpFC9F1FCE7DF02EBFFBBB05A2D04DF5C9" />
|
||||
<ComponentRef Id="cmp0A946DD3BC4835FBF8A86E6F14AFF450" />
|
||||
<ComponentRef Id="cmp304DB2D25065C3D5B1F99012C3F097B3" />
|
||||
<ComponentRef Id="cmpA1FAB8CA5E7B238E225525086D59754B" />
|
||||
<ComponentRef Id="cmp0D1074CD4A4DC67B6653DCF74385DF0E" />
|
||||
<ComponentRef Id="cmp9A9D29623257FA41663F84EAA2612688" />
|
||||
<ComponentRef Id="cmp45C131D18AAA1AE3EF9B48BCFCC28F6D" />
|
||||
<ComponentRef Id="cmpA829C4573EB498556AC47A36290EBF20" />
|
||||
<ComponentRef Id="cmp61CA0314C827E13940ED8588B8A8BA13" />
|
||||
<ComponentRef Id="cmpEDDED65BD9A61BB47433821AA149DC39" />
|
||||
<ComponentRef Id="cmpDAC39AC2BB8715FBE58F904CC2E9F698" />
|
||||
<ComponentRef Id="cmp316B7EADCCFA5F980015637E294FFC4B" />
|
||||
<ComponentRef Id="cmp0C61783999259F0182CE807EF9128FF4" />
|
||||
<ComponentRef Id="cmpB9B2B93E50693FCD82547A4EEDF59C47" />
|
||||
<ComponentRef Id="cmpE0368B606CE94148F4CA5F032A18B8AA" />
|
||||
<ComponentRef Id="cmpA478CC827F5FA0CCF27AAD818849CBD7" />
|
||||
<ComponentRef Id="cmp5878BDE96D855AFAF94266F3B9677E88" />
|
||||
<ComponentRef Id="cmp0759BB73FE6272908152EAACE25509E2" />
|
||||
<ComponentRef Id="cmp6B64D18EAB4E2AA54759D9616AB99FC6" />
|
||||
<ComponentRef Id="cmpE6ECB1DBD11FF2337C376EACE8AC5BC6" />
|
||||
<ComponentRef Id="cmpE6D8206385B0C897A99FF044F618C63E" />
|
||||
<ComponentRef Id="cmpD5BFC0D57560599B60E42E5501314FEB" />
|
||||
<ComponentRef Id="cmp82FF9A9C17F8D75F054D5CDDB8768C5C" />
|
||||
<ComponentRef Id="cmp396E4B41C7A33BE56F778473828172F7" />
|
||||
<ComponentRef Id="cmp02DF6A9A3BF862D35391F764FE67153F" />
|
||||
<ComponentRef Id="cmp701A262B1CE5139B2E57DF21A5AA3B94" />
|
||||
<ComponentRef Id="cmp1EAE9F3A379CF532AF1EBC883CDCD1D2" />
|
||||
<ComponentRef Id="cmp6C95449DAD236B0C9989FA43DD56CA85" />
|
||||
<ComponentRef Id="cmp11109A2A7F5FEC28295B469293762519" />
|
||||
<ComponentRef Id="cmp5B0A9912E5069950C30FC5FF8EAA16D4" />
|
||||
<ComponentRef Id="cmp1B37E98B173B46E1E5315143EF8D7728" />
|
||||
<ComponentRef Id="cmp2F414B6DDC2ADD0824C2DDE58E9102FA" />
|
||||
<ComponentRef Id="cmp3FB3F428740E959925E49E3155B50A4B" />
|
||||
<ComponentRef Id="cmpACC01C1F599CA1C44DC57D4D6199427D" />
|
||||
<ComponentRef Id="cmp3E80143A725EE7380FA78AA9E8BD2283" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -1,79 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*"
|
||||
Name="D330Viewer"
|
||||
Language="1033"
|
||||
Version="0.1.0.0"
|
||||
Manufacturer="Your Company"
|
||||
UpgradeCode="12345678-1234-1234-1234-123456789012">
|
||||
|
||||
<Package InstallerVersion="200"
|
||||
Compressed="yes"
|
||||
InstallScope="perMachine"
|
||||
Description="D330M Depth Camera Control System"
|
||||
Comments="Qt6-based depth camera control and visualization system" />
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<!-- 安装目录 -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFilesFolder">
|
||||
<Directory Id="INSTALLFOLDER" Name="D330Viewer" />
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="D330Viewer"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<!-- 开始菜单快捷方式 -->
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="ApplicationShortcut" Guid="12345678-1234-1234-1234-123456789013">
|
||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||
Name="D330Viewer"
|
||||
Description="D330M Depth Camera Control System"
|
||||
Target="[INSTALLFOLDER]D330Viewer.exe"
|
||||
WorkingDirectory="INSTALLFOLDER"/>
|
||||
<Shortcut Id="UninstallProduct"
|
||||
Name="卸载 D330Viewer"
|
||||
Description="卸载 D330M 深度相机控制系统"
|
||||
Target="[SystemFolder]msiexec.exe"
|
||||
Arguments="/x [ProductCode]"/>
|
||||
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall"/>
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\D330Viewer"
|
||||
Name="installed"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<!-- 用户配置注册表项(卸载时删除) -->
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="UserSettings" Guid="12345678-1234-1234-1234-123456789015">
|
||||
<RegistryKey Root="HKCU" Key="Software\D330Viewer\D330Viewer" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="placeholder" Value="1" KeyPath="yes"/>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="ProductFeature" Title="D330Viewer" Level="1">
|
||||
<ComponentGroupRef Id="ProductComponents" />
|
||||
<ComponentGroupRef Id="BinFiles" />
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
<ComponentRef Id="UserSettings" />
|
||||
</Feature>
|
||||
</Product>
|
||||
|
||||
<!-- 主程序文件 -->
|
||||
<Fragment>
|
||||
<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
|
||||
<Component Id="MainExecutable" Guid="12345678-1234-1234-1234-123456789014">
|
||||
<File Id="D330Viewer.exe"
|
||||
Source="$(var.BinDir)\D330Viewer.exe"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "config/ConfigManager.h"
|
||||
|
||||
ConfigManager::ConfigManager()
|
||||
: m_settings(std::make_unique<QSettings>("D330Viewer", "D330Viewer"))
|
||||
: m_settings(std::make_unique<QSettings>("Viewer", "Viewer"))
|
||||
{
|
||||
// 构造函数:初始化QSettings
|
||||
}
|
||||
@@ -45,7 +45,7 @@ void ConfigManager::setDataPort(int port)
|
||||
// ========== 相机配置 ==========
|
||||
int ConfigManager::getExposureTime() const
|
||||
{
|
||||
return m_settings->value("Camera/ExposureTime", 10000).toInt();
|
||||
return m_settings->value("Camera/ExposureTime", 5980).toInt();
|
||||
}
|
||||
|
||||
void ConfigManager::setExposureTime(int exposure)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "DeviceScanner.h"
|
||||
#include <QNetworkDatagram>
|
||||
#include <QNetworkInterface>
|
||||
#include <QCoreApplication>
|
||||
#include <QThread>
|
||||
#include <QDebug>
|
||||
|
||||
DeviceScanner::DeviceScanner(QObject *parent)
|
||||
@@ -8,8 +10,9 @@ DeviceScanner::DeviceScanner(QObject *parent)
|
||||
, m_socket(new QUdpSocket(this))
|
||||
, m_timeoutTimer(new QTimer(this))
|
||||
, m_scanTimer(new QTimer(this))
|
||||
, m_currentHost(HOST_START)
|
||||
, m_totalHosts(HOST_END - HOST_START + 1)
|
||||
, m_prefixLength(24)
|
||||
, m_currentHost(0)
|
||||
, m_totalHosts(0)
|
||||
, m_isScanning(false)
|
||||
{
|
||||
connect(m_socket, &QUdpSocket::readyRead, this, &DeviceScanner::onReadyRead);
|
||||
@@ -33,8 +36,9 @@ void DeviceScanner::startScan(const QString &subnet)
|
||||
return;
|
||||
}
|
||||
|
||||
m_subnet = subnet.isEmpty() ? getLocalSubnet() : subnet;
|
||||
m_currentHost = HOST_START;
|
||||
// Get network info (base IP and prefix length)
|
||||
getLocalNetworkInfo(m_baseIp, m_prefixLength);
|
||||
|
||||
m_foundDevices.clear();
|
||||
m_isScanning = true;
|
||||
|
||||
@@ -44,17 +48,62 @@ void DeviceScanner::startScan(const QString &subnet)
|
||||
return;
|
||||
}
|
||||
|
||||
qDebug() << "Starting fast device scan on subnet:" << m_subnet;
|
||||
|
||||
// Send DISCOVER to all hosts at once (batch mode)
|
||||
for (int host = HOST_START; host <= HOST_END; host++) {
|
||||
QString ip = m_subnet + "." + QString::number(host);
|
||||
sendDiscoveryPacket(ip);
|
||||
// Calculate IP range based on prefix length
|
||||
QStringList parts = m_baseIp.split('.');
|
||||
if (parts.size() != 4) {
|
||||
emit scanError("Invalid base IP");
|
||||
m_isScanning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait 3 seconds for all responses
|
||||
m_timeoutTimer->start(3000);
|
||||
qDebug() << "Sent discovery packets to all hosts, waiting for responses...";
|
||||
quint32 baseAddr = (parts[0].toUInt() << 24) | (parts[1].toUInt() << 16) |
|
||||
(parts[2].toUInt() << 8) | parts[3].toUInt();
|
||||
quint32 mask = (0xFFFFFFFF << (32 - m_prefixLength)) & 0xFFFFFFFF;
|
||||
quint32 networkAddr = baseAddr & mask;
|
||||
quint32 broadcastAddr = networkAddr | (~mask & 0xFFFFFFFF);
|
||||
|
||||
qDebug() << "Starting device scan - Base IP:" << m_baseIp << "Prefix:" << m_prefixLength;
|
||||
|
||||
int packetsSent = 0;
|
||||
|
||||
// For large subnets (/16 or larger), use broadcast + batched unicast
|
||||
if (m_prefixLength <= 16) {
|
||||
// First: send to subnet broadcast address
|
||||
QHostAddress broadcast(broadcastAddr);
|
||||
qDebug() << "Large subnet detected, using broadcast discovery:" << broadcast.toString();
|
||||
sendDiscoveryPacket(broadcast.toString());
|
||||
packetsSent++;
|
||||
|
||||
// Second: scan all /24 subnets with throttling to avoid buffer overflow
|
||||
qDebug() << "Scanning all /24 subnets within /16 range (throttled)...";
|
||||
for (int oct3 = 0; oct3 <= 255; oct3++) {
|
||||
quint32 subNetBase = (networkAddr & 0xFFFF0000) | (oct3 << 8);
|
||||
for (int oct4 = 1; oct4 <= 254; oct4++) {
|
||||
quint32 addr = subNetBase | oct4;
|
||||
sendDiscoveryPacket(QHostAddress(addr).toString());
|
||||
packetsSent++;
|
||||
}
|
||||
// Process events and add small delay every /24 subnet to avoid buffer overflow
|
||||
QCoreApplication::processEvents();
|
||||
QThread::msleep(5);
|
||||
}
|
||||
} else {
|
||||
// For smaller subnets, scan all hosts
|
||||
qDebug() << "Network range:" << QHostAddress(networkAddr + 1).toString()
|
||||
<< "to" << QHostAddress(broadcastAddr - 1).toString();
|
||||
|
||||
for (quint32 addr = networkAddr + 1; addr < broadcastAddr; addr++) {
|
||||
sendDiscoveryPacket(QHostAddress(addr).toString());
|
||||
packetsSent++;
|
||||
}
|
||||
}
|
||||
|
||||
m_totalHosts = packetsSent;
|
||||
qDebug() << "Sent" << packetsSent << "discovery packets, waiting for responses...";
|
||||
|
||||
// Adjust timeout based on network size
|
||||
int timeout = (m_prefixLength >= 24) ? 3000 : 10000;
|
||||
m_timeoutTimer->start(timeout);
|
||||
}
|
||||
|
||||
void DeviceScanner::stopScan()
|
||||
@@ -91,10 +140,17 @@ void DeviceScanner::onReadyRead()
|
||||
|
||||
qDebug() << "Received response from" << senderIp << ":" << response;
|
||||
|
||||
if (response.contains("D330M_CAMERA")) {
|
||||
if (response.startsWith("EXPOSURE:")) {
|
||||
bool ok;
|
||||
int exposure = response.mid(9).trimmed().toInt(&ok);
|
||||
if (ok && exposure > 0) {
|
||||
qDebug() << "Received exposure from camera:" << exposure << "us";
|
||||
emit exposureReceived(exposure);
|
||||
}
|
||||
} else if (response.contains("D330M_CAMERA")) {
|
||||
DeviceInfo device;
|
||||
device.ipAddress = senderIp;
|
||||
device.deviceName = "D330M Camera";
|
||||
device.deviceName = "Camera";
|
||||
device.port = SCAN_PORT;
|
||||
device.responseTime = 0;
|
||||
|
||||
@@ -122,7 +178,7 @@ void DeviceScanner::sendDiscoveryPacket(const QString &ip)
|
||||
m_socket->writeDatagram(data, QHostAddress(ip), SCAN_PORT);
|
||||
}
|
||||
|
||||
QString DeviceScanner::getLocalSubnet()
|
||||
void DeviceScanner::getLocalNetworkInfo(QString &baseIp, int &prefixLength)
|
||||
{
|
||||
// Get all network interfaces
|
||||
QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
|
||||
@@ -144,12 +200,14 @@ QString DeviceScanner::getLocalSubnet()
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found Ethernet adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
baseIp = addr.toString();
|
||||
prefixLength = entry.prefixLength();
|
||||
if (prefixLength <= 0 || prefixLength > 32) {
|
||||
prefixLength = 24; // Default to /24 if invalid
|
||||
}
|
||||
qDebug() << "Found Ethernet adapter:" << iface.humanReadableName()
|
||||
<< "IP:" << baseIp << "Prefix:" << prefixLength;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,15 +227,19 @@ QString DeviceScanner::getLocalSubnet()
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
baseIp = addr.toString();
|
||||
prefixLength = entry.prefixLength();
|
||||
if (prefixLength <= 0 || prefixLength > 32) {
|
||||
prefixLength = 24;
|
||||
}
|
||||
qDebug() << "Found adapter:" << iface.humanReadableName()
|
||||
<< "IP:" << baseIp << "Prefix:" << prefixLength;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "192.168.0";
|
||||
// Default fallback
|
||||
baseIp = "192.168.0.1";
|
||||
prefixLength = 24;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public:
|
||||
|
||||
signals:
|
||||
void deviceFound(const DeviceInfo &device);
|
||||
void exposureReceived(int exposureUs);
|
||||
void scanProgress(int current, int total);
|
||||
void scanFinished(int devicesFound);
|
||||
void scanError(const QString &error);
|
||||
@@ -40,13 +41,14 @@ private slots:
|
||||
|
||||
private:
|
||||
void sendDiscoveryPacket(const QString &ip);
|
||||
QString getLocalSubnet();
|
||||
void getLocalNetworkInfo(QString &baseIp, int &prefixLength);
|
||||
|
||||
QUdpSocket *m_socket;
|
||||
QTimer *m_timeoutTimer;
|
||||
QTimer *m_scanTimer;
|
||||
|
||||
QString m_subnet;
|
||||
QString m_baseIp;
|
||||
int m_prefixLength;
|
||||
int m_currentHost;
|
||||
int m_totalHosts;
|
||||
bool m_isScanning;
|
||||
@@ -55,8 +57,6 @@ private:
|
||||
|
||||
static constexpr int SCAN_PORT = 6790; // Control port for device discovery
|
||||
static constexpr int SCAN_TIMEOUT = 10;
|
||||
static constexpr int HOST_START = 1;
|
||||
static constexpr int HOST_END = 254;
|
||||
};
|
||||
|
||||
#endif // DEVICESCANNER_H
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
QString DeviceScanner::getLocalSubnet()
|
||||
{
|
||||
// Get all network interfaces
|
||||
QList<QNetworkInterface> interfaces = QNetworkInterface::allInterfaces();
|
||||
|
||||
// Priority 1: Look for Ethernet adapter (以太网)
|
||||
for (const QNetworkInterface &iface : interfaces) {
|
||||
QString name = iface.humanReadableName().toLower();
|
||||
|
||||
// Skip virtual adapters
|
||||
if (name.contains("virtual") || name.contains("vmware") ||
|
||||
name.contains("virtualbox") || name.contains("hyper-v") ||
|
||||
name.contains("vpn") || name.contains("tap") || name.contains("tun")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Look for Ethernet adapter
|
||||
if (name.contains("ethernet") || name.contains("以太网")) {
|
||||
QList<QNetworkAddressEntry> entries = iface.addressEntries();
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found Ethernet adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 2: Any non-virtual adapter
|
||||
for (const QNetworkInterface &iface : interfaces) {
|
||||
QString name = iface.humanReadableName().toLower();
|
||||
|
||||
if (name.contains("virtual") || name.contains("vmware") ||
|
||||
name.contains("virtualbox") || name.contains("hyper-v") ||
|
||||
name.contains("vpn") || name.contains("tap") || name.contains("tun")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QList<QNetworkAddressEntry> entries = iface.addressEntries();
|
||||
for (const QNetworkAddressEntry &entry : entries) {
|
||||
QHostAddress addr = entry.ip();
|
||||
if (addr.protocol() == QAbstractSocket::IPv4Protocol && !addr.isLoopback()) {
|
||||
QString ip = addr.toString();
|
||||
QStringList parts = ip.split('.');
|
||||
if (parts.size() == 4) {
|
||||
qDebug() << "Found adapter:" << iface.humanReadableName() << "IP:" << ip;
|
||||
return parts[0] + "." + parts[1] + "." + parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "192.168.0";
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
#include "core/GVSPParser.h"
|
||||
#include <QDebug>
|
||||
#include <QtConcurrent>
|
||||
#include <cstring>
|
||||
#include <winsock2.h>
|
||||
|
||||
GVSPParser::GVSPParser(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_isReceiving(false)
|
||||
, m_dataType(0)
|
||||
, m_currentBlockId(0)
|
||||
, m_expectedSize(0)
|
||||
, m_receivedSize(0)
|
||||
, m_imageWidth(0)
|
||||
, m_imageHeight(0)
|
||||
, m_pixelFormat(0)
|
||||
, m_lastBlockId(0)
|
||||
, m_packetCount(0)
|
||||
, m_imageSequence(0)
|
||||
{
|
||||
m_imageProcessingCount.storeRelaxed(0);
|
||||
}
|
||||
|
||||
GVSPParser::~GVSPParser()
|
||||
@@ -24,17 +18,18 @@ GVSPParser::~GVSPParser()
|
||||
|
||||
void GVSPParser::reset()
|
||||
{
|
||||
m_isReceiving = false;
|
||||
m_dataType = 0;
|
||||
m_currentBlockId = 0;
|
||||
m_dataBuffer.clear();
|
||||
m_expectedSize = 0;
|
||||
m_receivedSize = 0;
|
||||
m_packetCount = 0;
|
||||
m_leftIRState = StreamState();
|
||||
m_rightIRState = StreamState();
|
||||
m_rgbState = StreamState();
|
||||
m_depthState = StreamState();
|
||||
m_pointCloudState = StreamState();
|
||||
m_imageSequence = 0;
|
||||
}
|
||||
|
||||
void GVSPParser::parsePacket(const QByteArray &packet)
|
||||
{
|
||||
static int debugCount = 0;
|
||||
|
||||
if (packet.size() < sizeof(GVSPPacketHeader)) {
|
||||
return;
|
||||
}
|
||||
@@ -42,27 +37,20 @@ void GVSPParser::parsePacket(const QByteArray &packet)
|
||||
const uint8_t *data = reinterpret_cast<const uint8_t*>(packet.constData());
|
||||
const GVSPPacketHeader *header = reinterpret_cast<const GVSPPacketHeader*>(data);
|
||||
|
||||
// 注释掉调试日志以提高性能
|
||||
// static int debugCount = 0;
|
||||
// if (debugCount < 3) {
|
||||
// qDebug() << "Packet" << debugCount << "first 16 bytes (hex):";
|
||||
// QString hexStr;
|
||||
// for (int i = 0; i < qMin(16, packet.size()); i++) {
|
||||
// hexStr += QString("%1 ").arg(data[i], 2, 16, QChar('0'));
|
||||
// }
|
||||
// qDebug() << hexStr;
|
||||
// debugCount++;
|
||||
// }
|
||||
|
||||
// GVSP包头格式:status(2) + block_id(2) + packet_format(4)
|
||||
// 包类型在packet_format的高字节
|
||||
uint32_t packet_fmt = ntohl(header->packet_fmt_id);
|
||||
uint8_t packetType = (packet_fmt >> 24) & 0xFF;
|
||||
// GigE Vision 2.1 标准 GVSP 包头解析
|
||||
uint16_t blockId = ntohs(header->block_id);
|
||||
uint8_t packetType = header->packet_format;
|
||||
uint16_t packetId = ntohs(header->packet_id);
|
||||
|
||||
// 注释掉频繁的日志输出以提高性能
|
||||
// static int leaderCount = 0;
|
||||
// static int trailerCount = 0;
|
||||
// 打印前5个包的详细信息
|
||||
if (debugCount < 5) {
|
||||
// qDebug() << "[GVSPParser] Packet" << debugCount
|
||||
// << "Type:" << packetType
|
||||
// << "BlockID:" << blockId
|
||||
// << "PacketID:" << packetId
|
||||
// << "Size:" << packet.size();
|
||||
debugCount++;
|
||||
}
|
||||
|
||||
switch (packetType) {
|
||||
case GVSP_LEADER_PACKET:
|
||||
@@ -81,12 +69,14 @@ void GVSPParser::parsePacket(const QByteArray &packet)
|
||||
|
||||
void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size)
|
||||
{
|
||||
if (size < sizeof(GVSPPacketHeader) + sizeof(GVSPImageDataLeader)) {
|
||||
// 最小大小检查:至少要有包头 + 2字节reserved + 2字节payload_type
|
||||
if (size < sizeof(GVSPPacketHeader) + 4) {
|
||||
qDebug() << "[GVSPParser] Leader packet too small:" << size << "bytes";
|
||||
return;
|
||||
}
|
||||
|
||||
const GVSPPacketHeader *header = reinterpret_cast<const GVSPPacketHeader*>(data);
|
||||
m_currentBlockId = ntohs(header->block_id);
|
||||
uint32_t blockId = ntohs(header->block_id);
|
||||
|
||||
// Check payload type
|
||||
const uint16_t *payload_type_ptr = reinterpret_cast<const uint16_t*>(data + sizeof(GVSPPacketHeader) + 2);
|
||||
@@ -94,49 +84,110 @@ void GVSPParser::handleLeaderPacket(const uint8_t *data, size_t size)
|
||||
|
||||
if (payload_type == PAYLOAD_TYPE_IMAGE) {
|
||||
// Image data leader
|
||||
if (size < sizeof(GVSPPacketHeader) + sizeof(GVSPImageDataLeader)) {
|
||||
qDebug() << "[GVSPParser] Image leader too small";
|
||||
return;
|
||||
}
|
||||
const GVSPImageDataLeader *leader = reinterpret_cast<const GVSPImageDataLeader*>(data + sizeof(GVSPPacketHeader));
|
||||
|
||||
m_dataType = 1;
|
||||
m_imageWidth = ntohl(leader->size_x);
|
||||
m_imageHeight = ntohl(leader->size_y);
|
||||
m_pixelFormat = ntohl(leader->pixel_format);
|
||||
m_expectedSize = m_imageWidth * m_imageHeight * 2; // 12-bit packed in 16-bit
|
||||
|
||||
m_dataBuffer.clear();
|
||||
m_dataBuffer.reserve(m_expectedSize);
|
||||
m_receivedSize = 0;
|
||||
m_isReceiving = true;
|
||||
m_packetCount = 0;
|
||||
|
||||
// 注释掉频繁的日志输出
|
||||
// qDebug() << "Image Leader: Block" << m_currentBlockId
|
||||
// << "Size:" << m_imageWidth << "x" << m_imageHeight;
|
||||
uint32_t imageWidth = ntohl(leader->size_x);
|
||||
uint32_t imageHeight = ntohl(leader->size_y);
|
||||
uint32_t pixelFormat = ntohl(leader->pixel_format);
|
||||
|
||||
// 根据像素格式选择对应的状态
|
||||
StreamState *state = nullptr;
|
||||
if (pixelFormat == PIXEL_FORMAT_MONO16_LEFT || pixelFormat == PIXEL_FORMAT_MONO8_LEFT) {
|
||||
state = &m_leftIRState;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO16_RIGHT || pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
state = &m_rightIRState;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MJPEG) {
|
||||
state = &m_rgbState;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO16 || pixelFormat == PIXEL_FORMAT_12BIT_GRAY) {
|
||||
// Legacy格式:根据序号分配
|
||||
int imageType = m_imageSequence % 2;
|
||||
state = (imageType == 0) ? &m_leftIRState : &m_rightIRState;
|
||||
}
|
||||
|
||||
if (state) {
|
||||
state->blockId = blockId;
|
||||
state->imageWidth = imageWidth;
|
||||
state->imageHeight = imageHeight;
|
||||
state->pixelFormat = pixelFormat;
|
||||
|
||||
// 根据pixel format设置期望大小
|
||||
if (pixelFormat == PIXEL_FORMAT_MJPEG) {
|
||||
// MJPEG是压缩格式,实际大小未知,设置为0表示动态接收
|
||||
state->expectedSize = 0;
|
||||
} else if (pixelFormat == PIXEL_FORMAT_MONO8_LEFT || pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
// 8-bit灰度格式(下采样)
|
||||
state->expectedSize = imageWidth * imageHeight;
|
||||
} else {
|
||||
// 16-bit或12-bit灰度等固定格式
|
||||
state->expectedSize = imageWidth * imageHeight * 2;
|
||||
}
|
||||
|
||||
state->dataBuffer.clear();
|
||||
if (state->expectedSize > 0) {
|
||||
state->dataBuffer.reserve(state->expectedSize);
|
||||
}
|
||||
state->receivedSize = 0;
|
||||
state->isReceiving = true;
|
||||
state->packetCount = 0;
|
||||
}
|
||||
}
|
||||
else if (payload_type == PAYLOAD_TYPE_BINARY) {
|
||||
// Depth data leader
|
||||
const GVSPBinaryDataLeader *leader = reinterpret_cast<const GVSPBinaryDataLeader*>(data + sizeof(GVSPPacketHeader));
|
||||
|
||||
m_dataType = 3;
|
||||
m_expectedSize = ntohl(leader->file_size);
|
||||
|
||||
m_dataBuffer.clear();
|
||||
m_dataBuffer.reserve(m_expectedSize);
|
||||
m_receivedSize = 0;
|
||||
m_isReceiving = true;
|
||||
m_packetCount = 0;
|
||||
|
||||
// 注释掉频繁的日志输出
|
||||
// qDebug() << "Depth Leader: Block" << m_currentBlockId
|
||||
// << "Size:" << m_expectedSize << "bytes";
|
||||
m_depthState.blockId = blockId;
|
||||
m_depthState.expectedSize = ntohl(leader->file_size);
|
||||
m_depthState.dataBuffer.clear();
|
||||
m_depthState.dataBuffer.reserve(m_depthState.expectedSize);
|
||||
m_depthState.receivedSize = 0;
|
||||
m_depthState.isReceiving = true;
|
||||
m_depthState.packetCount = 0;
|
||||
}
|
||||
else if (payload_type == PAYLOAD_TYPE_POINTCLOUD) {
|
||||
// Point cloud data leader (vendor-specific 0x8000)
|
||||
const GVSPPointCloudDataLeader *leader = reinterpret_cast<const GVSPPointCloudDataLeader*>(data + sizeof(GVSPPacketHeader));
|
||||
|
||||
m_pointCloudState.blockId = blockId;
|
||||
m_pointCloudState.expectedSize = ntohl(leader->data_size);
|
||||
m_pointCloudState.dataBuffer.clear();
|
||||
m_pointCloudState.dataBuffer.reserve(m_pointCloudState.expectedSize);
|
||||
m_pointCloudState.receivedSize = 0;
|
||||
m_pointCloudState.isReceiving = true;
|
||||
m_pointCloudState.packetCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void GVSPParser::handlePayloadPacket(const uint8_t *data, size_t size)
|
||||
{
|
||||
if (!m_isReceiving) {
|
||||
if (size < sizeof(GVSPPacketHeader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const GVSPPacketHeader *header = reinterpret_cast<const GVSPPacketHeader*>(data);
|
||||
uint32_t blockId = ntohs(header->block_id);
|
||||
|
||||
// 查找匹配的状态
|
||||
StreamState *state = nullptr;
|
||||
if (m_leftIRState.isReceiving && m_leftIRState.blockId == blockId) {
|
||||
state = &m_leftIRState;
|
||||
} else if (m_rightIRState.isReceiving && m_rightIRState.blockId == blockId) {
|
||||
state = &m_rightIRState;
|
||||
} else if (m_rgbState.isReceiving && m_rgbState.blockId == blockId) {
|
||||
state = &m_rgbState;
|
||||
} else if (m_depthState.isReceiving && m_depthState.blockId == blockId) {
|
||||
state = &m_depthState;
|
||||
} else if (m_pointCloudState.isReceiving && m_pointCloudState.blockId == blockId) {
|
||||
state = &m_pointCloudState;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return; // 没有匹配的接收状态
|
||||
}
|
||||
|
||||
if (size <= sizeof(GVSPPacketHeader)) {
|
||||
return;
|
||||
}
|
||||
@@ -146,84 +197,213 @@ void GVSPParser::handlePayloadPacket(const uint8_t *data, size_t size)
|
||||
size_t payload_size = size - sizeof(GVSPPacketHeader);
|
||||
|
||||
// Append to buffer
|
||||
m_dataBuffer.append(reinterpret_cast<const char*>(payload), payload_size);
|
||||
m_receivedSize += payload_size;
|
||||
m_packetCount++;
|
||||
state->dataBuffer.append(reinterpret_cast<const char*>(payload), payload_size);
|
||||
state->receivedSize += payload_size;
|
||||
state->packetCount++;
|
||||
}
|
||||
|
||||
void GVSPParser::handleTrailerPacket(const uint8_t *data, size_t size)
|
||||
{
|
||||
if (!m_isReceiving) {
|
||||
if (size < sizeof(GVSPPacketHeader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 注释掉频繁的日志输出
|
||||
// qDebug() << "Trailer received: Block" << m_currentBlockId
|
||||
// << "Received" << m_receivedSize << "/" << m_expectedSize << "bytes"
|
||||
// << "Packets:" << m_packetCount;
|
||||
const GVSPPacketHeader *header = reinterpret_cast<const GVSPPacketHeader*>(data);
|
||||
uint32_t blockId = ntohs(header->block_id);
|
||||
|
||||
// Process complete data
|
||||
if (m_dataType == 1) {
|
||||
processImageData();
|
||||
} else if (m_dataType == 3) {
|
||||
processDepthData();
|
||||
// 查找匹配的状态并处理
|
||||
if (m_leftIRState.isReceiving && m_leftIRState.blockId == blockId) {
|
||||
processImageData(&m_leftIRState);
|
||||
m_leftIRState.isReceiving = false;
|
||||
m_lastBlockId = blockId;
|
||||
} else if (m_rightIRState.isReceiving && m_rightIRState.blockId == blockId) {
|
||||
processImageData(&m_rightIRState);
|
||||
m_rightIRState.isReceiving = false;
|
||||
m_lastBlockId = blockId;
|
||||
} else if (m_rgbState.isReceiving && m_rgbState.blockId == blockId) {
|
||||
processImageData(&m_rgbState);
|
||||
m_rgbState.isReceiving = false;
|
||||
m_lastBlockId = blockId;
|
||||
} else if (m_depthState.isReceiving && m_depthState.blockId == blockId) {
|
||||
processDepthData(&m_depthState);
|
||||
m_depthState.isReceiving = false;
|
||||
m_lastBlockId = blockId;
|
||||
} else if (m_pointCloudState.isReceiving && m_pointCloudState.blockId == blockId) {
|
||||
processPointCloudData(&m_pointCloudState);
|
||||
m_pointCloudState.isReceiving = false;
|
||||
m_lastBlockId = blockId;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
m_isReceiving = false;
|
||||
m_lastBlockId = m_currentBlockId;
|
||||
}
|
||||
|
||||
void GVSPParser::processImageData()
|
||||
void GVSPParser::processImageData(GVSPParser::StreamState *state)
|
||||
{
|
||||
if (m_dataBuffer.size() < m_expectedSize) {
|
||||
// 注释掉频繁的警告日志
|
||||
// qDebug() << "Warning: Incomplete image data" << m_dataBuffer.size() << "/" << m_expectedSize;
|
||||
if (!state) return;
|
||||
|
||||
// 处理MJPEG格式(RGB相机)
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MJPEG) {
|
||||
// RGB MJPEG
|
||||
emit rgbImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert 16-bit depth data to 8-bit grayscale for display
|
||||
const uint16_t *src = reinterpret_cast<const uint16_t*>(m_dataBuffer.constData());
|
||||
QImage image(m_imageWidth, m_imageHeight, QImage::Format_Grayscale8);
|
||||
|
||||
// Find min/max for normalization
|
||||
uint16_t minVal = 65535, maxVal = 0;
|
||||
for (size_t i = 0; i < m_imageWidth * m_imageHeight; i++) {
|
||||
uint16_t val = src[i];
|
||||
if (val > 0) {
|
||||
if (val < minVal) minVal = val;
|
||||
if (val > maxVal) maxVal = val;
|
||||
// 处理Mono16格式(左右红外相机原始16位数据)
|
||||
// 使用像素格式直接区分左右,不依赖接收顺序
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO16_LEFT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 左红外原始数据
|
||||
emit leftImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize to 0-255 and flip vertically
|
||||
uint8_t *dst = image.bits();
|
||||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO16_RIGHT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 右红外原始数据
|
||||
emit rightImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t y = 0; y < m_imageHeight; y++) {
|
||||
for (size_t x = 0; x < m_imageWidth; x++) {
|
||||
size_t src_idx = y * m_imageWidth + x;
|
||||
size_t dst_idx = (m_imageHeight - 1 - y) * m_imageWidth + x; // 垂直翻转
|
||||
// 处理Mono8格式(左右红外相机下采样8位数据)
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO8_LEFT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 左红外8位数据
|
||||
emit leftImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t val = src[src_idx];
|
||||
if (val == 0) {
|
||||
dst[dst_idx] = 0;
|
||||
} else {
|
||||
dst[dst_idx] = static_cast<uint8_t>((val - minVal) * scale);
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO8_RIGHT) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
// 右红外8位数据
|
||||
emit rightImageReceived(state->dataBuffer, state->blockId);
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 兼容旧版本:使用序号区分(legacy)
|
||||
if (state->pixelFormat == PIXEL_FORMAT_MONO16) {
|
||||
// 检查数据大小
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据图像序号区分:偶数=左红外,奇数=右红外
|
||||
int imageType = m_imageSequence % 2;
|
||||
|
||||
if (imageType == 0) {
|
||||
// 左红外原始数据
|
||||
emit leftImageReceived(state->dataBuffer, state->blockId);
|
||||
} else {
|
||||
// 右红外原始数据
|
||||
emit rightImageReceived(state->dataBuffer, state->blockId);
|
||||
}
|
||||
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理12-bit灰度格式 - 固定大小,需要检查
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理12-bit灰度图像(左右红外相机)
|
||||
if (state->pixelFormat != PIXEL_FORMAT_12BIT_GRAY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 节流机制:如果已有3个或更多图像在处理中,跳过当前帧
|
||||
if (m_imageProcessingCount.loadAcquire() >= 3) {
|
||||
m_imageSequence++;
|
||||
return;
|
||||
}
|
||||
|
||||
// 增加处理计数
|
||||
m_imageProcessingCount.ref();
|
||||
|
||||
// 复制数据到局部变量
|
||||
QByteArray dataCopy = state->dataBuffer;
|
||||
uint32_t blockId = state->blockId;
|
||||
size_t width = state->imageWidth;
|
||||
size_t height = state->imageHeight;
|
||||
int imageSeq = m_imageSequence;
|
||||
|
||||
// 使用QtConcurrent在后台线程处理图像数据
|
||||
QtConcurrent::run([this, dataCopy, blockId, width, height, imageSeq]() {
|
||||
// Convert 16-bit depth data to 8-bit grayscale for display
|
||||
const uint16_t *src = reinterpret_cast<const uint16_t*>(dataCopy.constData());
|
||||
QImage image(width, height, QImage::Format_Grayscale8);
|
||||
|
||||
// 优化:使用采样方式快速估算min/max(每隔16个像素采样一次)
|
||||
uint16_t minVal = 65535, maxVal = 0;
|
||||
size_t totalPixels = width * height;
|
||||
const size_t sampleStep = 16; // 采样步长
|
||||
|
||||
for (size_t i = 0; i < totalPixels; i += sampleStep) {
|
||||
uint16_t val = src[i];
|
||||
if (val > 0) {
|
||||
if (val < minVal) minVal = val;
|
||||
if (val > maxVal) maxVal = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emit imageReceived(image, m_currentBlockId);
|
||||
// 归一化(不翻转,保持原始方向)
|
||||
uint8_t *dst = image.bits();
|
||||
float scale = (maxVal > minVal) ? (255.0f / (maxVal - minVal)) : 0.0f;
|
||||
|
||||
for (size_t row = 0; row < height; row++) {
|
||||
for (size_t col = 0; col < width; col++) {
|
||||
size_t idx = row * width + col;
|
||||
uint16_t val = src[idx];
|
||||
dst[idx] = (val == 0) ? 0 : static_cast<uint8_t>((val - minVal) * scale);
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:此代码路径已废弃,下位机现在发送JPEG格式
|
||||
// 仅保留兼容旧代码的信号
|
||||
emit imageReceived(image, blockId);
|
||||
|
||||
// 减少处理计数
|
||||
m_imageProcessingCount.deref();
|
||||
});
|
||||
|
||||
// 增加图像序号
|
||||
m_imageSequence++;
|
||||
}
|
||||
|
||||
void GVSPParser::processDepthData()
|
||||
void GVSPParser::processDepthData(GVSPParser::StreamState *state)
|
||||
{
|
||||
if (m_dataBuffer.size() < m_expectedSize) {
|
||||
// 注释掉频繁的警告日志
|
||||
// qDebug() << "Warning: Incomplete depth data" << m_dataBuffer.size() << "/" << m_expectedSize;
|
||||
if (!state) return;
|
||||
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
emit depthDataReceived(m_dataBuffer, m_currentBlockId);
|
||||
emit depthDataReceived(state->dataBuffer, state->blockId);
|
||||
}
|
||||
|
||||
void GVSPParser::processPointCloudData(GVSPParser::StreamState *state)
|
||||
{
|
||||
if (!state) return;
|
||||
|
||||
if (state->dataBuffer.size() < state->expectedSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 点云数据直接发送,格式为 short 数组 (x, y, z, x, y, z, ...)
|
||||
emit pointCloudDataReceived(state->dataBuffer, state->blockId);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Logger* Logger::instance()
|
||||
|
||||
Logger::Logger(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_maxLines(10000) // 保留最新10000行
|
||||
, m_maxLines(100000) // 保留最新10000行
|
||||
, m_currentLines(0)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -16,9 +16,16 @@ NetworkManager::NetworkManager(QObject *parent)
|
||||
connect(m_dataSocket, &QUdpSocket::readyRead, this, &NetworkManager::onReadyRead);
|
||||
connect(m_dataSocket, &QUdpSocket::errorOccurred, this, &NetworkManager::onError);
|
||||
|
||||
// 连接控制socket接收信号(用于接收相机回复,如曝光值)
|
||||
connect(m_controlSocket, &QUdpSocket::readyRead, this, &NetworkManager::onControlReadyRead);
|
||||
|
||||
// 连接GVSP解析器信号
|
||||
connect(m_gvspParser, &GVSPParser::imageReceived, this, &NetworkManager::imageReceived);
|
||||
connect(m_gvspParser, &GVSPParser::leftImageReceived, this, &NetworkManager::leftImageReceived);
|
||||
connect(m_gvspParser, &GVSPParser::rightImageReceived, this, &NetworkManager::rightImageReceived);
|
||||
connect(m_gvspParser, &GVSPParser::rgbImageReceived, this, &NetworkManager::rgbImageReceived);
|
||||
connect(m_gvspParser, &GVSPParser::depthDataReceived, this, &NetworkManager::depthDataReceived);
|
||||
connect(m_gvspParser, &GVSPParser::pointCloudDataReceived, this, &NetworkManager::pointCloudDataReceived);
|
||||
}
|
||||
|
||||
NetworkManager::~NetworkManager()
|
||||
@@ -34,7 +41,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
m_dataPort = dataPort;
|
||||
|
||||
// 绑定控制Socket到任意端口(让系统自动分配)
|
||||
if (!m_controlSocket->bind(QHostAddress::Any, 0)) {
|
||||
if(!m_controlSocket->bind(QHostAddress::Any, 0)) {
|
||||
QString error = QString("Failed to bind control socket: %1")
|
||||
.arg(m_controlSocket->errorString());
|
||||
qDebug() << error;
|
||||
@@ -44,7 +51,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
qDebug() << "Successfully bound control socket to port" << m_controlSocket->localPort();
|
||||
|
||||
// 绑定数据接收端口
|
||||
if (!m_dataSocket->bind(QHostAddress::Any, m_dataPort)) {
|
||||
if(!m_dataSocket->bind(QHostAddress::Any, m_dataPort)) {
|
||||
QString error = QString("Failed to bind data port %1: %2")
|
||||
.arg(m_dataPort)
|
||||
.arg(m_dataSocket->errorString());
|
||||
@@ -53,8 +60,8 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置UDP接收缓冲区大小为64MB(减少丢包)
|
||||
m_dataSocket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, QVariant(64 * 1024 * 1024));
|
||||
// 设置UDP接收缓冲区大小为256MB(最大化,减少丢包)
|
||||
m_dataSocket->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, QVariant(256 * 1024 * 1024));
|
||||
|
||||
qDebug() << "Successfully bound data port" << m_dataPort;
|
||||
qDebug() << "Data socket state:" << m_dataSocket->state();
|
||||
@@ -63,6 +70,10 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
m_isConnected = true;
|
||||
qDebug() << "Connected to camera:" << m_cameraIp << "Control port:" << m_controlPort << "Data port:" << m_dataPort;
|
||||
|
||||
// Send DISCOVER to get camera's current exposure value
|
||||
sendCommand("DISCOVER");
|
||||
qDebug() << "Sent DISCOVER to fetch camera exposure";
|
||||
|
||||
// Send STOP command to register client IP on camera
|
||||
sendStopCommand();
|
||||
qDebug() << "Sent STOP command to register client IP";
|
||||
@@ -73,7 +84,7 @@ bool NetworkManager::connectToCamera(const QString &ip, int controlPort, int dat
|
||||
|
||||
void NetworkManager::disconnectFromCamera()
|
||||
{
|
||||
if (m_isConnected) {
|
||||
if(m_isConnected) {
|
||||
m_controlSocket->close();
|
||||
m_dataSocket->close();
|
||||
m_isConnected = false;
|
||||
@@ -90,7 +101,7 @@ bool NetworkManager::isConnected() const
|
||||
// ========== 发送控制命令 ==========
|
||||
bool NetworkManager::sendCommand(const QString &command)
|
||||
{
|
||||
if (!m_isConnected) {
|
||||
if(!m_isConnected) {
|
||||
qDebug() << "Not connected to camera";
|
||||
return false;
|
||||
}
|
||||
@@ -105,7 +116,7 @@ bool NetworkManager::sendCommand(const QString &command)
|
||||
qint64 sent = m_controlSocket->writeDatagram(data, QHostAddress(m_cameraIp), m_controlPort);
|
||||
|
||||
qDebug() << "writeDatagram returned:" << sent;
|
||||
if (sent == -1) {
|
||||
if(sent == -1) {
|
||||
QString error = QString("Failed to send command: %1").arg(m_controlSocket->errorString());
|
||||
qDebug() << error;
|
||||
qDebug() << "Socket error code:" << m_controlSocket->error();
|
||||
@@ -128,15 +139,51 @@ bool NetworkManager::sendStopCommand()
|
||||
return sendCommand("STOP");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendOnceCommand()
|
||||
{
|
||||
return sendCommand("ONCE");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendExposureCommand(int exposureTime)
|
||||
{
|
||||
QString command = QString("EXPOSURE:%1").arg(exposureTime);
|
||||
return sendCommand(command);
|
||||
QString exposureCommand = QString("EXPOSURE:%1").arg(exposureTime);
|
||||
return sendCommand(exposureCommand);
|
||||
}
|
||||
|
||||
// ========== 传输开关命令 ==========
|
||||
bool NetworkManager::sendEnableLeftIR()
|
||||
{
|
||||
return sendCommand("ENABLE_LEFT");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendDisableLeftIR()
|
||||
{
|
||||
return sendCommand("DISABLE_LEFT");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendEnableRightIR()
|
||||
{
|
||||
return sendCommand("ENABLE_RIGHT");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendDisableRightIR()
|
||||
{
|
||||
return sendCommand("DISABLE_RIGHT");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendEnableRGB()
|
||||
{
|
||||
return sendCommand("ENABLE_RGB");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendDisableRGB()
|
||||
{
|
||||
return sendCommand("DISABLE_RGB");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendMonocularMode()
|
||||
{
|
||||
return sendCommand("MONOCULAR");
|
||||
}
|
||||
|
||||
bool NetworkManager::sendBinocularMode()
|
||||
{
|
||||
return sendCommand("BINOCULAR");
|
||||
}
|
||||
|
||||
// ========== 槽函数 ==========
|
||||
@@ -152,9 +199,11 @@ void NetworkManager::onReadyRead()
|
||||
quint16 senderPort;
|
||||
m_dataSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort);
|
||||
|
||||
// 临时添加日志以诊断问题
|
||||
if (packetCount < 5) {
|
||||
qDebug() << "[NetworkManager] Received packet" << packetCount << "from" << sender.toString() << ":" << senderPort << "size:" << datagram.size();
|
||||
// 只打印前5个包的详细信息
|
||||
if(packetCount < 5) {
|
||||
// qDebug() << "[NetworkManager] Packet" << packetCount
|
||||
// << "from" << sender.toString() << ":" << senderPort
|
||||
// << "size:" << datagram.size() << "bytes";
|
||||
}
|
||||
packetCount++;
|
||||
|
||||
@@ -164,6 +213,32 @@ void NetworkManager::onReadyRead()
|
||||
// 仍然发出原始数据信号(用于调试)
|
||||
emit dataReceived(datagram);
|
||||
}
|
||||
|
||||
// 每1000个包打印一次统计(减少日志量)
|
||||
if(packetCount % 1000 == 0) {
|
||||
// qDebug() << "[NetworkManager] Total packets received:" << packetCount;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::onControlReadyRead()
|
||||
{
|
||||
while (m_controlSocket->hasPendingDatagrams()) {
|
||||
QByteArray datagram;
|
||||
datagram.resize(m_controlSocket->pendingDatagramSize());
|
||||
m_controlSocket->readDatagram(datagram.data(), datagram.size());
|
||||
|
||||
QString response = QString::fromUtf8(datagram);
|
||||
qDebug() << "[NetworkManager] Control response:" << response;
|
||||
|
||||
if (response.startsWith("EXPOSURE:")) {
|
||||
bool ok;
|
||||
int exposure = response.mid(9).trimmed().toInt(&ok);
|
||||
if (ok && exposure > 0) {
|
||||
qDebug() << "[NetworkManager] Camera exposure:" << exposure << "us";
|
||||
emit exposureReceived(exposure);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::onError(QAbstractSocket::SocketError socketError)
|
||||
|
||||
@@ -26,19 +26,36 @@ public:
|
||||
bool sendCommand(const QString &command);
|
||||
bool sendStartCommand();
|
||||
bool sendStopCommand();
|
||||
bool sendOnceCommand();
|
||||
bool sendExposureCommand(int exposureTime);
|
||||
|
||||
// 发送传输开关命令
|
||||
bool sendEnableLeftIR();
|
||||
bool sendDisableLeftIR();
|
||||
bool sendEnableRightIR();
|
||||
bool sendDisableRightIR();
|
||||
bool sendEnableRGB();
|
||||
bool sendDisableRGB();
|
||||
|
||||
// 发送单目/双目模式切换命令
|
||||
bool sendMonocularMode();
|
||||
bool sendBinocularMode();
|
||||
|
||||
signals:
|
||||
void connected();
|
||||
void disconnected();
|
||||
void exposureReceived(int exposureUs);
|
||||
void errorOccurred(const QString &error);
|
||||
void dataReceived(const QByteArray &data);
|
||||
void imageReceived(const QImage &image, uint32_t blockId);
|
||||
void leftImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void rightImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void rgbImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void depthDataReceived(const QByteArray &depthData, uint32_t blockId);
|
||||
void pointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId);
|
||||
|
||||
private slots:
|
||||
void onReadyRead();
|
||||
void onControlReadyRead();
|
||||
void onError(QAbstractSocket::SocketError socketError);
|
||||
|
||||
private:
|
||||
|
||||
@@ -1,6 +1,92 @@
|
||||
#include "core/PointCloudProcessor.h"
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <pcl/common/point_tests.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
struct VoxelKey {
|
||||
int x;
|
||||
int y;
|
||||
int z;
|
||||
|
||||
bool operator==(const VoxelKey &other) const noexcept
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
};
|
||||
|
||||
struct VoxelKeyHash {
|
||||
size_t operator()(const VoxelKey &k) const noexcept
|
||||
{
|
||||
// FNV-1a hash for 3D integer voxel index.
|
||||
uint64_t h = 1469598103934665603ull;
|
||||
auto mix = [&h](uint64_t v) {
|
||||
h ^= v;
|
||||
h *= 1099511628211ull;
|
||||
};
|
||||
mix(static_cast<uint32_t>(k.x));
|
||||
mix(static_cast<uint32_t>(k.y));
|
||||
mix(static_cast<uint32_t>(k.z));
|
||||
return static_cast<size_t>(h);
|
||||
}
|
||||
};
|
||||
|
||||
struct VoxelAccum {
|
||||
float sumX = 0.0f;
|
||||
float sumY = 0.0f;
|
||||
float sumZ = 0.0f;
|
||||
uint32_t count = 0;
|
||||
};
|
||||
|
||||
constexpr int kNeighborOffsets[26][3] = {
|
||||
{-1, -1, -1}, {0, -1, -1}, {1, -1, -1},
|
||||
{-1, 0, -1}, {0, 0, -1}, {1, 0, -1},
|
||||
{-1, 1, -1}, {0, 1, -1}, {1, 1, -1},
|
||||
{-1, -1, 0}, {0, -1, 0}, {1, -1, 0},
|
||||
{-1, 0, 0}, {1, 0, 0},
|
||||
{-1, 1, 0}, {0, 1, 0}, {1, 1, 0},
|
||||
{-1, -1, 1}, {0, -1, 1}, {1, -1, 1},
|
||||
{-1, 0, 1}, {0, 0, 1}, {1, 0, 1},
|
||||
{-1, 1, 1}, {0, 1, 1}, {1, 1, 1}
|
||||
};
|
||||
|
||||
bool readFloatFile(const QString &path, std::vector<float> &out)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray raw = file.readAll();
|
||||
const QString text = QString::fromUtf8(raw);
|
||||
const QStringList tokens = text.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||||
out.clear();
|
||||
out.reserve(tokens.size());
|
||||
|
||||
for (const QString &token : tokens) {
|
||||
bool ok = false;
|
||||
float value = token.toFloat(&ok);
|
||||
if (ok) {
|
||||
out.push_back(value);
|
||||
}
|
||||
}
|
||||
|
||||
return !out.empty();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PointCloudProcessor::PointCloudProcessor(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -21,7 +107,26 @@ PointCloudProcessor::PointCloudProcessor(QObject *parent)
|
||||
, m_depthBuffer(nullptr)
|
||||
, m_xyzBuffer(nullptr)
|
||||
, m_clInitialized(false)
|
||||
, m_denoiseEnabled(false)
|
||||
, m_voxelLeafSize(2.5f)
|
||||
, m_denoiseNeighborSupport(6)
|
||||
, m_denoiseLowTailPermille(15)
|
||||
, m_denoiseDepthBandPermille(80)
|
||||
, m_k1(0.0f)
|
||||
, m_k2(0.0f)
|
||||
, m_p1(0.0f)
|
||||
, m_p2(0.0f)
|
||||
, m_p5(1.0f / 1432.8957f)
|
||||
, m_p6(-637.5117f / 1432.8957f)
|
||||
, m_p7(1.0f / 1432.6590f)
|
||||
, m_p8(-521.8720f / 1432.6590f)
|
||||
, m_hasLowerCalibration(false)
|
||||
, m_hasAnchorMeanZ(false)
|
||||
, m_anchorMeanZFiltered(0.0f)
|
||||
, m_hasLowCutZ(false)
|
||||
, m_lowCutZFiltered(0.0f)
|
||||
{
|
||||
loadLowerCalibration();
|
||||
}
|
||||
|
||||
PointCloudProcessor::~PointCloudProcessor()
|
||||
@@ -35,6 +140,14 @@ void PointCloudProcessor::setCameraIntrinsics(float fx, float fy, float cx, floa
|
||||
m_fy = fy;
|
||||
m_cx = cx;
|
||||
m_cy = cy;
|
||||
|
||||
// Keep lower-machine style projection terms in sync when intrinsics are changed at runtime.
|
||||
if (m_fx != 0.0f && m_fy != 0.0f) {
|
||||
m_p5 = 1.0f / m_fx;
|
||||
m_p6 = -m_cx / m_fx;
|
||||
m_p7 = 1.0f / m_fy;
|
||||
m_p8 = -m_cy / m_fy;
|
||||
}
|
||||
}
|
||||
|
||||
void PointCloudProcessor::setZScaleFactor(float scale)
|
||||
@@ -42,6 +155,557 @@ void PointCloudProcessor::setZScaleFactor(float scale)
|
||||
m_zScale = scale;
|
||||
}
|
||||
|
||||
void PointCloudProcessor::loadLowerCalibration()
|
||||
{
|
||||
const QString appDir = QCoreApplication::applicationDirPath();
|
||||
QStringList candidates;
|
||||
candidates
|
||||
<< QDir::current().filePath("cmos0")
|
||||
<< QDir(appDir).filePath("cmos0")
|
||||
<< QDir(appDir).filePath("../cmos0")
|
||||
<< QDir(appDir).filePath("../../cmos0");
|
||||
|
||||
for (const QString &dirPath : candidates) {
|
||||
const QString kcPath = QDir(dirPath).filePath("kc.txt");
|
||||
const QString kkPath = QDir(dirPath).filePath("KK.txt");
|
||||
if (!QFile::exists(kcPath) || !QFile::exists(kkPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<float> kcVals;
|
||||
std::vector<float> kkVals;
|
||||
if (!readFloatFile(kcPath, kcVals) || !readFloatFile(kkPath, kkVals)) {
|
||||
continue;
|
||||
}
|
||||
if (kcVals.size() < 4 || kkVals.size() < 6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float fx = kkVals[0];
|
||||
const float cx = kkVals[2];
|
||||
const float fy = kkVals[4];
|
||||
const float cy = kkVals[5];
|
||||
if (fx == 0.0f || fy == 0.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_k1 = kcVals[0];
|
||||
m_k2 = kcVals[1];
|
||||
m_p1 = kcVals[2];
|
||||
m_p2 = kcVals[3];
|
||||
|
||||
m_fx = fx;
|
||||
m_fy = fy;
|
||||
m_cx = cx;
|
||||
m_cy = cy;
|
||||
m_p5 = 1.0f / fx;
|
||||
m_p6 = -cx / fx;
|
||||
m_p7 = 1.0f / fy;
|
||||
m_p8 = -cy / fy;
|
||||
m_hasLowerCalibration = true;
|
||||
|
||||
qDebug() << "[PointCloud] Loaded lower calibration from" << dirPath
|
||||
<< "kc size:" << static_cast<int>(kcVals.size())
|
||||
<< "KK size:" << static_cast<int>(kkVals.size());
|
||||
return;
|
||||
}
|
||||
|
||||
m_hasLowerCalibration = false;
|
||||
qDebug() << "[PointCloud] lower calibration txt not found, using fallback intrinsics";
|
||||
}
|
||||
|
||||
void PointCloudProcessor::setDenoiseEnabled(bool enabled)
|
||||
{
|
||||
m_denoiseEnabled.store(enabled, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PointCloudProcessor::setDenoiseNeighborSupport(int minNeighbors)
|
||||
{
|
||||
m_denoiseNeighborSupport.store(std::clamp(minNeighbors, 3, 12), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PointCloudProcessor::setDenoiseLowTailPermille(int permille)
|
||||
{
|
||||
m_denoiseLowTailPermille.store(std::clamp(permille, 5, 50), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void PointCloudProcessor::setDenoiseDepthBandPermille(int permille)
|
||||
{
|
||||
m_denoiseDepthBandPermille.store(std::clamp(permille, 40, 180), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr PointCloudProcessor::applyDenoise(
|
||||
const pcl::PointCloud<pcl::PointXYZ>::Ptr &input)
|
||||
{
|
||||
if (!input || input->empty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const size_t total = input->points.size();
|
||||
const int width = static_cast<int>(input->width);
|
||||
const int height = static_cast<int>(input->height);
|
||||
const int supportMinNeighbors = m_denoiseNeighborSupport.load(std::memory_order_relaxed);
|
||||
const int lowTailPermille = m_denoiseLowTailPermille.load(std::memory_order_relaxed);
|
||||
const int depthBandPermille = m_denoiseDepthBandPermille.load(std::memory_order_relaxed);
|
||||
|
||||
bool hasPrevAnchor = false;
|
||||
float prevAnchorMeanZ = 0.0f;
|
||||
bool hasPrevLowCut = false;
|
||||
float prevLowCutZ = 0.0f;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_denoiseStateMutex);
|
||||
hasPrevAnchor = m_hasAnchorMeanZ;
|
||||
prevAnchorMeanZ = m_anchorMeanZFiltered;
|
||||
hasPrevLowCut = m_hasLowCutZ;
|
||||
prevLowCutZ = m_lowCutZFiltered;
|
||||
}
|
||||
bool anchorUpdated = false;
|
||||
float anchorToStore = 0.0f;
|
||||
bool lowCutUpdated = false;
|
||||
float lowCutToStore = 0.0f;
|
||||
|
||||
// Fallback for unorganized clouds: only remove invalid points.
|
||||
if (width <= 1 || height <= 1 || static_cast<size_t>(width) * static_cast<size_t>(height) != total) {
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr validOnly(new pcl::PointCloud<pcl::PointXYZ>());
|
||||
validOnly->points.reserve(total);
|
||||
for (const auto &p : input->points) {
|
||||
if (pcl::isFinite(p) && p.z > 0.0f) {
|
||||
validOnly->points.push_back(p);
|
||||
}
|
||||
}
|
||||
if (validOnly->points.empty()) {
|
||||
return input;
|
||||
}
|
||||
validOnly->width = static_cast<uint32_t>(validOnly->points.size());
|
||||
validOnly->height = 1;
|
||||
validOnly->is_dense = true;
|
||||
return validOnly;
|
||||
}
|
||||
|
||||
const auto idx = [width](int r, int c) -> int { return r * width + c; };
|
||||
std::vector<uint8_t> validMask(total, 0);
|
||||
|
||||
float minZ = std::numeric_limits<float>::max();
|
||||
float maxZ = std::numeric_limits<float>::lowest();
|
||||
int validCount = 0;
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
const auto &p = input->points[i];
|
||||
if (pcl::isFinite(p) && p.z > 0.0f) {
|
||||
validMask[i] = 1;
|
||||
++validCount;
|
||||
if (p.z < minZ) minZ = p.z;
|
||||
if (p.z > maxZ) maxZ = p.z;
|
||||
}
|
||||
}
|
||||
|
||||
if (validCount < 1200) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Pass 0: adaptive conditional depth gate (inspired by ConditionalRemoval/CropBox idea).
|
||||
// Use center ROI median depth as reference, then keep a dynamic depth band.
|
||||
const float spanZ = std::max(0.0f, maxZ - minZ);
|
||||
int gatedCount = validCount;
|
||||
if (spanZ > 1e-3f) {
|
||||
const int r0 = static_cast<int>(height * 0.35f);
|
||||
const int r1 = static_cast<int>(height * 0.75f);
|
||||
const int c0 = static_cast<int>(width * 0.35f);
|
||||
const int c1 = static_cast<int>(width * 0.65f);
|
||||
|
||||
std::vector<float> centerZ;
|
||||
centerZ.reserve(static_cast<size_t>((r1 - r0) * (c1 - c0)));
|
||||
for (int r = r0; r < r1; ++r) {
|
||||
for (int c = c0; c < c1; ++c) {
|
||||
const int i = idx(r, c);
|
||||
if (validMask[i]) {
|
||||
centerZ.push_back(input->points[i].z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (centerZ.size() > 800) {
|
||||
const size_t mid = centerZ.size() / 2;
|
||||
std::nth_element(centerZ.begin(), centerZ.begin() + mid, centerZ.end());
|
||||
const float zRef = centerZ[mid];
|
||||
|
||||
const float zNear = std::max(minZ, zRef - std::max(80.0f, spanZ * 0.08f));
|
||||
const float zFar = std::min(maxZ, zRef + std::max(250.0f, spanZ * 0.22f));
|
||||
|
||||
gatedCount = 0;
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
if (!validMask[i]) {
|
||||
continue;
|
||||
}
|
||||
const float z = input->points[i].z;
|
||||
if (z < zNear || z > zFar) {
|
||||
validMask[i] = 0;
|
||||
} else {
|
||||
++gatedCount;
|
||||
}
|
||||
}
|
||||
|
||||
// If gate is too aggressive, rollback.
|
||||
if (gatedCount < 2000) {
|
||||
std::fill(validMask.begin(), validMask.end(), 0);
|
||||
gatedCount = 0;
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
const auto &p = input->points[i];
|
||||
if (pcl::isFinite(p) && p.z > 0.0f) {
|
||||
validMask[i] = 1;
|
||||
++gatedCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1: local depth-consistency support in 3x3 neighborhood.
|
||||
// This suppresses thin ray artifacts while preserving contiguous surfaces.
|
||||
std::vector<uint8_t> supportMask(total, 0);
|
||||
int supportCount = 0;
|
||||
|
||||
for (int r = 2; r < height - 2; ++r) {
|
||||
for (int c = 2; c < width - 2; ++c) {
|
||||
const int center = idx(r, c);
|
||||
if (!validMask[center]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const float z = input->points[center].z;
|
||||
const float dzThreshold = std::max(12.0f, z * 0.015f);
|
||||
|
||||
int neighbors = 0;
|
||||
for (int rr = -2; rr <= 2; ++rr) {
|
||||
for (int cc = -2; cc <= 2; ++cc) {
|
||||
if (rr == 0 && cc == 0) {
|
||||
continue;
|
||||
}
|
||||
const int ni = idx(r + rr, c + cc);
|
||||
if (!validMask[ni]) {
|
||||
continue;
|
||||
}
|
||||
if (std::fabs(input->points[ni].z - z) <= dzThreshold) {
|
||||
++neighbors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (neighbors >= supportMinNeighbors) {
|
||||
supportMask[center] = 1;
|
||||
++supportCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (supportCount < 1500) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// Pass 1.5: one morphology-like cleanup to remove sparse remnants.
|
||||
std::vector<uint8_t> cleanMask = supportMask;
|
||||
int cleanCount = 0;
|
||||
for (int r = 1; r < height - 1; ++r) {
|
||||
for (int c = 1; c < width - 1; ++c) {
|
||||
const int center = idx(r, c);
|
||||
if (!supportMask[center]) {
|
||||
continue;
|
||||
}
|
||||
int kept = 0;
|
||||
for (int rr = -1; rr <= 1; ++rr) {
|
||||
for (int cc = -1; cc <= 1; ++cc) {
|
||||
if (rr == 0 && cc == 0) {
|
||||
continue;
|
||||
}
|
||||
if (supportMask[idx(r + rr, c + cc)]) {
|
||||
++kept;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (kept < 2) {
|
||||
cleanMask[center] = 0;
|
||||
}
|
||||
if (cleanMask[center]) {
|
||||
++cleanCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cleanCount >= 1500) {
|
||||
supportMask.swap(cleanMask);
|
||||
supportCount = cleanCount;
|
||||
}
|
||||
|
||||
// Pass 2: clip the near-camera low-depth tail to suppress center-radiating streaks.
|
||||
std::vector<uint8_t> finalMask = supportMask;
|
||||
if (spanZ > 1e-3f) {
|
||||
constexpr int kBins = 256;
|
||||
std::vector<int> hist(kBins, 0);
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
if (!supportMask[i]) {
|
||||
continue;
|
||||
}
|
||||
const float z = input->points[i].z;
|
||||
int b = static_cast<int>((z - minZ) / spanZ * static_cast<float>(kBins - 1));
|
||||
b = std::clamp(b, 0, kBins - 1);
|
||||
++hist[b];
|
||||
}
|
||||
|
||||
const int lowTailTarget = std::max(120, static_cast<int>(supportCount * (static_cast<float>(lowTailPermille) / 1000.0f)));
|
||||
int accum = 0;
|
||||
int lowBin = 0;
|
||||
for (int b = 0; b < kBins; ++b) {
|
||||
accum += hist[b];
|
||||
if (accum >= lowTailTarget) {
|
||||
lowBin = b;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const float rawLowCut = minZ + spanZ * (static_cast<float>(lowBin) / static_cast<float>(kBins - 1));
|
||||
float zLowCut = rawLowCut;
|
||||
if (hasPrevLowCut) {
|
||||
const float maxJump = std::max(120.0f, spanZ * 0.10f);
|
||||
const float clamped = std::clamp(rawLowCut, prevLowCutZ - maxJump, prevLowCutZ + maxJump);
|
||||
zLowCut = prevLowCutZ * 0.65f + clamped * 0.35f;
|
||||
}
|
||||
lowCutUpdated = true;
|
||||
lowCutToStore = zLowCut;
|
||||
|
||||
int finalCount = 0;
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
if (finalMask[i] && input->points[i].z < zLowCut) {
|
||||
finalMask[i] = 0;
|
||||
}
|
||||
if (finalMask[i]) {
|
||||
++finalCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalCount < 1200) {
|
||||
finalMask = supportMask;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2.5: 2D connected components + foreground-priority keep.
|
||||
// This suppresses surrounding residual blobs while preserving the near main object.
|
||||
struct ComponentStat {
|
||||
std::vector<int> pixels;
|
||||
int area = 0;
|
||||
float zSum = 0.0f;
|
||||
int centerOverlap = 0;
|
||||
};
|
||||
|
||||
const int centerR0 = static_cast<int>(height * 0.35f);
|
||||
const int centerR1 = static_cast<int>(height * 0.75f);
|
||||
const int centerC0 = static_cast<int>(width * 0.35f);
|
||||
const int centerC1 = static_cast<int>(width * 0.65f);
|
||||
|
||||
std::vector<uint8_t> visited(total, 0);
|
||||
std::vector<ComponentStat> comps;
|
||||
comps.reserve(32);
|
||||
|
||||
std::vector<int> queue;
|
||||
queue.reserve(4096);
|
||||
constexpr int cdr[8] = {-1, -1, -1, 0, 0, 1, 1, 1};
|
||||
constexpr int cdc[8] = {-1, 0, 1, -1, 1, -1, 0, 1};
|
||||
|
||||
for (int r = 0; r < height; ++r) {
|
||||
for (int c = 0; c < width; ++c) {
|
||||
const int seed = idx(r, c);
|
||||
if (!finalMask[seed] || visited[seed]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ComponentStat comp;
|
||||
queue.clear();
|
||||
queue.push_back(seed);
|
||||
visited[seed] = 1;
|
||||
|
||||
for (size_t head = 0; head < queue.size(); ++head) {
|
||||
const int cur = queue[head];
|
||||
const int rr = cur / width;
|
||||
const int cc = cur % width;
|
||||
|
||||
comp.pixels.push_back(cur);
|
||||
comp.area += 1;
|
||||
comp.zSum += input->points[cur].z;
|
||||
if (rr >= centerR0 && rr < centerR1 && cc >= centerC0 && cc < centerC1) {
|
||||
comp.centerOverlap += 1;
|
||||
}
|
||||
|
||||
for (int k = 0; k < 8; ++k) {
|
||||
const int nr = rr + cdr[k];
|
||||
const int nc = cc + cdc[k];
|
||||
if (nr < 0 || nr >= height || nc < 0 || nc >= width) {
|
||||
continue;
|
||||
}
|
||||
const int ni = idx(nr, nc);
|
||||
if (!finalMask[ni] || visited[ni]) {
|
||||
continue;
|
||||
}
|
||||
visited[ni] = 1;
|
||||
queue.push_back(ni);
|
||||
}
|
||||
}
|
||||
|
||||
comps.push_back(std::move(comp));
|
||||
}
|
||||
}
|
||||
|
||||
if (!comps.empty()) {
|
||||
int anchor = -1;
|
||||
float anchorMeanZ = std::numeric_limits<float>::max();
|
||||
int anchorArea = 0;
|
||||
float bestScore = -std::numeric_limits<float>::max();
|
||||
const float temporalBand = std::max(120.0f, spanZ * 0.10f);
|
||||
|
||||
// Stable anchor selection with temporal bias to reduce frame-to-frame jumping.
|
||||
for (int i = 0; i < static_cast<int>(comps.size()); ++i) {
|
||||
const auto &cp = comps[i];
|
||||
if (cp.area < 300) {
|
||||
continue;
|
||||
}
|
||||
const float meanZ = cp.zSum / static_cast<float>(cp.area);
|
||||
float score = static_cast<float>(cp.centerOverlap) * 6.0f
|
||||
+ static_cast<float>(std::min(cp.area, 12000)) * 0.25f;
|
||||
if (hasPrevAnchor) {
|
||||
const float dz = std::fabs(meanZ - prevAnchorMeanZ);
|
||||
score -= dz * 0.35f;
|
||||
if (dz <= temporalBand) {
|
||||
score += 1200.0f;
|
||||
}
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
anchor = i;
|
||||
anchorMeanZ = meanZ;
|
||||
anchorArea = cp.area;
|
||||
}
|
||||
}
|
||||
|
||||
if (anchor >= 0) {
|
||||
float stableAnchorMeanZ = anchorMeanZ;
|
||||
if (hasPrevAnchor) {
|
||||
const float maxJump = std::max(180.0f, spanZ * 0.15f);
|
||||
const float clamped = std::clamp(anchorMeanZ, prevAnchorMeanZ - maxJump, prevAnchorMeanZ + maxJump);
|
||||
stableAnchorMeanZ = prevAnchorMeanZ * 0.60f + clamped * 0.40f;
|
||||
}
|
||||
anchorUpdated = true;
|
||||
anchorToStore = stableAnchorMeanZ;
|
||||
|
||||
std::vector<uint8_t> ccMask(total, 0);
|
||||
int kept = 0;
|
||||
// Make depth-band slider more perceptible: 40 -> tight (strong suppression), 180 -> loose.
|
||||
const float bandT = std::clamp((static_cast<float>(depthBandPermille) - 40.0f) / 140.0f, 0.0f, 1.0f);
|
||||
const float zKeepBandFloor = 90.0f + 260.0f * bandT;
|
||||
const float zKeepBandSpanFactor = 0.03f + 0.19f * bandT;
|
||||
const float zKeepBandBase = std::max(zKeepBandFloor, spanZ * zKeepBandSpanFactor);
|
||||
const float zKeepBand = hasPrevAnchor
|
||||
? (zKeepBandBase * (1.15f + 0.35f * bandT))
|
||||
: (zKeepBandBase * (1.00f + 0.25f * bandT));
|
||||
const float minKeepAreaRatio = 0.035f - 0.020f * bandT;
|
||||
const int minKeepArea = std::max(60, static_cast<int>(anchorArea * minKeepAreaRatio));
|
||||
|
||||
for (const auto &cp : comps) {
|
||||
if (cp.area < minKeepArea) {
|
||||
continue;
|
||||
}
|
||||
const float meanZ = cp.zSum / static_cast<float>(cp.area);
|
||||
const float overlapBonus = (cp.centerOverlap > 0) ? (zKeepBand * 0.45f) : 0.0f;
|
||||
if (meanZ > stableAnchorMeanZ + zKeepBand + overlapBonus) {
|
||||
continue;
|
||||
}
|
||||
for (int p : cp.pixels) {
|
||||
ccMask[p] = 1;
|
||||
++kept;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply when preserving a reasonable part of support mask to avoid frame jumps.
|
||||
const float minStableRatio = 0.18f - 0.10f * bandT;
|
||||
const int minStableKeep = std::max(1000, static_cast<int>(supportCount * minStableRatio));
|
||||
if (kept >= minStableKeep) {
|
||||
// Soft fallback: keep previously accepted support points that are depth-consistent.
|
||||
const float softKeepFar = stableAnchorMeanZ + zKeepBand * (1.20f + 1.60f * bandT);
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
if (finalMask[i] && !ccMask[i] && input->points[i].z <= softKeepFar) {
|
||||
ccMask[i] = 1;
|
||||
}
|
||||
}
|
||||
finalMask.swap(ccMask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2.8: final spur cleanup to reduce radiating thin points.
|
||||
{
|
||||
std::vector<uint8_t> pruned = finalMask;
|
||||
int keptAfterPrune = 0;
|
||||
const float bandT = std::clamp((static_cast<float>(depthBandPermille) - 40.0f) / 140.0f, 0.0f, 1.0f);
|
||||
const float nearRaySpanFactor = 0.10f - 0.06f * bandT;
|
||||
const float nearRayOffset = std::max(70.0f, spanZ * nearRaySpanFactor);
|
||||
const float nearRayGate = lowCutUpdated
|
||||
? (lowCutToStore + nearRayOffset)
|
||||
: (minZ + spanZ * (0.22f - 0.10f * bandT));
|
||||
for (int r = 1; r < height - 1; ++r) {
|
||||
for (int c = 1; c < width - 1; ++c) {
|
||||
const int center = idx(r, c);
|
||||
if (!finalMask[center]) {
|
||||
continue;
|
||||
}
|
||||
int n = 0;
|
||||
for (int rr = -1; rr <= 1; ++rr) {
|
||||
for (int cc = -1; cc <= 1; ++cc) {
|
||||
if (rr == 0 && cc == 0) {
|
||||
continue;
|
||||
}
|
||||
if (finalMask[idx(r + rr, c + cc)]) {
|
||||
++n;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (n < 2 && input->points[center].z < nearRayGate) {
|
||||
pruned[center] = 0;
|
||||
}
|
||||
if (pruned[center]) {
|
||||
++keptAfterPrune;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (keptAfterPrune >= 1200) {
|
||||
finalMask.swap(pruned);
|
||||
}
|
||||
}
|
||||
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr denoised(new pcl::PointCloud<pcl::PointXYZ>());
|
||||
denoised->points.reserve(static_cast<size_t>(supportCount));
|
||||
for (size_t i = 0; i < total; ++i) {
|
||||
if (finalMask[i]) {
|
||||
denoised->points.push_back(input->points[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (denoised->points.empty()) {
|
||||
return input;
|
||||
}
|
||||
|
||||
denoised->width = static_cast<uint32_t>(denoised->points.size());
|
||||
denoised->height = 1;
|
||||
denoised->is_dense = true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_denoiseStateMutex);
|
||||
if (anchorUpdated) {
|
||||
m_anchorMeanZFiltered = anchorToStore;
|
||||
m_hasAnchorMeanZ = true;
|
||||
}
|
||||
if (lowCutUpdated) {
|
||||
m_lowCutZFiltered = lowCutToStore;
|
||||
m_hasLowCutZ = true;
|
||||
}
|
||||
}
|
||||
|
||||
return denoised;
|
||||
}
|
||||
|
||||
bool PointCloudProcessor::initializeOpenCL()
|
||||
{
|
||||
if (m_clInitialized) {
|
||||
@@ -101,9 +765,8 @@ bool PointCloudProcessor::initializeOpenCL()
|
||||
"int y = idx / width; "
|
||||
"int x = idx % width; "
|
||||
"float z = depth[idx] * z_scale; "
|
||||
// 完全平面的圆柱投影:X和Y直接使用像素坐标,缩放到合适的范围
|
||||
"xyz[idx*3] = (x - cx) * 2.0f; " // X坐标,缩放系数2.0
|
||||
"xyz[idx*3+1] = (y - cy) * 2.0f; " // Y坐标,缩放系数2.0
|
||||
"xyz[idx*3] = (x - cx) * z * inv_fx; "
|
||||
"xyz[idx*3+1] = (y - cy) * z * inv_fy; "
|
||||
"xyz[idx*3+2] = z; "
|
||||
"}";
|
||||
|
||||
@@ -243,6 +906,103 @@ void PointCloudProcessor::processDepthData(const QByteArray &depthData, uint32_t
|
||||
|
||||
// 注释掉频繁的日志输出
|
||||
// qDebug() << "[PointCloud] Block" << blockId << "processed successfully";
|
||||
if (m_denoiseEnabled.load(std::memory_order_relaxed)) {
|
||||
cloud = applyDenoise(cloud);
|
||||
}
|
||||
emit pointCloudReady(cloud, blockId);
|
||||
}
|
||||
|
||||
void PointCloudProcessor::processPointCloudData(const QByteArray &cloudData, uint32_t blockId)
|
||||
{
|
||||
// qDebug() << "[PointCloud] Processing pre-computed point cloud data";
|
||||
// qDebug() << "[PointCloud] Data size:" << cloudData.size() << "bytes";
|
||||
|
||||
// 验证数据大小:支持两种格式
|
||||
// 格式1:只有Z坐标 (width * height * sizeof(int16_t))
|
||||
// 格式2:完整XYZ (width * height * 3 * sizeof(int16_t))
|
||||
size_t expectedSizeZ = m_imageWidth * m_imageHeight * sizeof(int16_t);
|
||||
size_t expectedSizeXYZ = m_imageWidth * m_imageHeight * 3 * sizeof(int16_t);
|
||||
|
||||
bool isZOnly = (cloudData.size() == expectedSizeZ);
|
||||
bool isXYZ = (cloudData.size() == expectedSizeXYZ);
|
||||
|
||||
if (!isZOnly && !isXYZ) {
|
||||
qDebug() << "[PointCloud] ERROR: Invalid point cloud data size:" << cloudData.size()
|
||||
<< "expected:" << expectedSizeZ << "(Z only) or" << expectedSizeXYZ << "(XYZ)";
|
||||
return;
|
||||
}
|
||||
|
||||
// qDebug() << "[PointCloud] Data format:" << (isZOnly ? "Z only" : "XYZ");
|
||||
|
||||
// 创建PCL点云
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
|
||||
cloud->width = m_imageWidth;
|
||||
cloud->height = m_imageHeight;
|
||||
cloud->is_dense = false;
|
||||
cloud->points.resize(m_totalPoints);
|
||||
|
||||
// 从int16_t数组读取点云数据
|
||||
const int16_t* cloudShort = reinterpret_cast<const int16_t*>(cloudData.constData());
|
||||
|
||||
// 与下位机 gpu_calculate_pointcloud.cl 对齐的参数:
|
||||
// u = p5 * (j - 0.5) + p6, v = p7 * (i + 1) + p8
|
||||
// (k1,k2,p1,p2,p5,p6,p7,p8) 由 cmos0/kc.txt 与 cmos0/KK.txt 加载。
|
||||
|
||||
if (isZOnly) {
|
||||
// Z-only格式:标准针孔模型反投影
|
||||
for (size_t i = 0; i < m_totalPoints; i++) {
|
||||
int row = i / m_imageWidth;
|
||||
int col = i % m_imageWidth;
|
||||
|
||||
float z = static_cast<float>(cloudShort[i]) * m_zScale;
|
||||
|
||||
// 旧公式保留,便于快速回退:
|
||||
// cloud->points[i].x = (col - m_cx) * z * inv_fx;
|
||||
// cloud->points[i].y = (row - m_cy) * z * inv_fy;
|
||||
|
||||
// 下位机同款:先求(u,v)并做畸变修正得到(unc,vnc),再乘z得到(x,y)
|
||||
float u = m_p5 * (static_cast<float>(col) - 0.5f) + m_p6;
|
||||
float v = m_p7 * (static_cast<float>(row) + 1.0f) + m_p8;
|
||||
float r = u * u + v * v;
|
||||
float temp3 = 1.0f / (1.0f + m_k1 * r + m_k2 * r * r);
|
||||
float unc = temp3 * (u - 2.0f * m_p1 * u * v - m_p2 * (r + 2.0f * u * u));
|
||||
float vnc = temp3 * (v - m_p1 * (r + 2.0f * v * v) - 2.0f * m_p2 * u * v);
|
||||
|
||||
cloud->points[i].x = unc * z;
|
||||
cloud->points[i].y = vnc * z;
|
||||
cloud->points[i].z = z;
|
||||
}
|
||||
} else {
|
||||
// XYZ格式:使用Z值进行针孔模型反投影
|
||||
for (size_t i = 0; i < m_totalPoints; i++) {
|
||||
int row = i / m_imageWidth;
|
||||
int col = i % m_imageWidth;
|
||||
|
||||
float z = static_cast<float>(cloudShort[i * 3 + 2]) * m_zScale;
|
||||
|
||||
// 旧公式保留,便于快速回退:
|
||||
// cloud->points[i].x = (col - m_cx) * z * inv_fx;
|
||||
// cloud->points[i].y = (row - m_cy) * z * inv_fy;
|
||||
|
||||
// 下位机同款:先求(u,v)并做畸变修正得到(unc,vnc),再乘z得到(x,y)
|
||||
float u = m_p5 * (static_cast<float>(col) - 0.5f) + m_p6;
|
||||
float v = m_p7 * (static_cast<float>(row) + 1.0f) + m_p8;
|
||||
float r = u * u + v * v;
|
||||
float temp3 = 1.0f / (1.0f + m_k1 * r + m_k2 * r * r);
|
||||
float unc = temp3 * (u - 2.0f * m_p1 * u * v - m_p2 * (r + 2.0f * u * u));
|
||||
float vnc = temp3 * (v - m_p1 * (r + 2.0f * v * v) - 2.0f * m_p2 * u * v);
|
||||
|
||||
cloud->points[i].x = unc * z;
|
||||
cloud->points[i].y = vnc * z;
|
||||
cloud->points[i].z = z;
|
||||
}
|
||||
}
|
||||
|
||||
// qDebug() << "[PointCloud] Block" << blockId << "processed successfully,"
|
||||
// << m_totalPoints << "points";
|
||||
if (m_denoiseEnabled.load(std::memory_order_relaxed)) {
|
||||
cloud = applyDenoise(cloud);
|
||||
}
|
||||
emit pointCloudReady(cloud, blockId);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -35,7 +35,6 @@ private slots:
|
||||
// 相机控制槽函数
|
||||
void onStartClicked();
|
||||
void onStopClicked();
|
||||
void onOnceClicked();
|
||||
void onExposureChanged(int value);
|
||||
void onCaptureClicked();
|
||||
void onBrowseSavePathClicked();
|
||||
@@ -51,7 +50,11 @@ private slots:
|
||||
|
||||
// GVSP数据处理槽函数
|
||||
void onImageReceived(const QImage &image, uint32_t blockId);
|
||||
void onLeftImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void onRightImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void onRgbImageReceived(const QByteArray &jpegData, uint32_t blockId);
|
||||
void onDepthDataReceived(const QByteArray &depthData, uint32_t blockId);
|
||||
void onPointCloudDataReceived(const QByteArray &cloudData, uint32_t blockId);
|
||||
void onPointCloudReady(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud, uint32_t blockId);
|
||||
|
||||
// 设备扫描槽函数
|
||||
@@ -83,7 +86,9 @@ private:
|
||||
bool savePointCloud(const QString &dir, const QString &baseName, const QString &format);
|
||||
void performBackgroundSave(const QString &saveDir, const QString &baseName,
|
||||
const QString &depthFormat, const QString &pointCloudFormat,
|
||||
const QImage &image, pcl::PointCloud<pcl::PointXYZ>::Ptr cloud);
|
||||
const QImage &image, pcl::PointCloud<pcl::PointXYZ>::Ptr cloud,
|
||||
const QByteArray &leftIRData, const QByteArray &rightIRData,
|
||||
const QByteArray &rgbData);
|
||||
|
||||
// 日志辅助函数
|
||||
void addLog(const QString &message, const QString &level = "INFO");
|
||||
@@ -101,28 +106,47 @@ private:
|
||||
|
||||
// 状态变量
|
||||
bool m_isConnected;
|
||||
bool m_autoSaveOnNextFrame;
|
||||
|
||||
// 当前帧数据(用于拍照)
|
||||
QImage m_currentImage;
|
||||
pcl::PointCloud<pcl::PointXYZ>::Ptr m_currentPointCloud;
|
||||
uint32_t m_currentFrameId;
|
||||
|
||||
// 三个相机的原始JPEG数据(用于保存完整分辨率图像)
|
||||
QByteArray m_currentLeftIRData;
|
||||
QByteArray m_currentRightIRData;
|
||||
QByteArray m_currentRgbData;
|
||||
|
||||
// UI控件指针
|
||||
class QWidget *m_centralWidget;
|
||||
class QSplitter *m_mainSplitter;
|
||||
class QWidget *m_controlPanel;
|
||||
class QLabel *m_imageDisplay;
|
||||
class QLabel *m_imageDisplay; // 兼容旧代码
|
||||
PointCloudWidget *m_pointCloudWidget;
|
||||
|
||||
// 三个图像显示控件
|
||||
QLabel *m_leftImageDisplay;
|
||||
QLabel *m_rightImageDisplay;
|
||||
QLabel *m_rgbImageDisplay;
|
||||
|
||||
// 按钮控件
|
||||
QPushButton *m_refreshBtn;
|
||||
QPushButton *m_connectBtn;
|
||||
QPushButton *m_startBtn;
|
||||
QPushButton *m_stopBtn;
|
||||
QPushButton *m_onceBtn;
|
||||
QPushButton *m_captureBtn;
|
||||
|
||||
|
||||
// 数据传输开关按钮
|
||||
QPushButton *m_leftIRToggle;
|
||||
QPushButton *m_rightIRToggle;
|
||||
QPushButton *m_rgbToggle;
|
||||
QPushButton *m_pointCloudColorToggle; // 点云颜色开关
|
||||
QPushButton *m_pointCloudDenoiseToggle; // Point cloud denoise toggle
|
||||
|
||||
// 单目/双目模式切换按钮
|
||||
QPushButton *m_monocularBtn;
|
||||
QPushButton *m_binocularBtn;
|
||||
|
||||
// 输入控件
|
||||
QSlider *m_exposureSlider;
|
||||
QSpinBox *m_exposureSpinBox;
|
||||
@@ -134,26 +158,81 @@ private:
|
||||
QPushButton *m_browseSavePathBtn;
|
||||
class QComboBox *m_depthFormatCombo;
|
||||
class QComboBox *m_pointCloudFormatCombo;
|
||||
QSlider *m_denoiseSupportSlider;
|
||||
QSpinBox *m_denoiseSupportSpinBox;
|
||||
QSlider *m_denoiseTailSlider;
|
||||
QSpinBox *m_denoiseTailSpinBox;
|
||||
QSlider *m_denoiseBandSlider;
|
||||
QSpinBox *m_denoiseBandSpinBox;
|
||||
|
||||
// 显示控件
|
||||
QLabel *m_statusLabel;
|
||||
QListWidget *m_deviceList;
|
||||
|
||||
// 统计信息控件
|
||||
QLabel *m_fpsLabel;
|
||||
QLabel *m_depthFpsLabel;
|
||||
QLabel *m_pointCloudFpsLabel;
|
||||
QLabel *m_resolutionLabel;
|
||||
QLabel *m_queueLabel;
|
||||
|
||||
// 三个相机的FPS标签
|
||||
QLabel *m_leftFpsLabel;
|
||||
QLabel *m_rightFpsLabel;
|
||||
QLabel *m_rgbFpsLabel;
|
||||
|
||||
// 日志显示控件
|
||||
class QTextEdit *m_logDisplay;
|
||||
QPushButton *m_clearLogBtn;
|
||||
QPushButton *m_saveLogBtn;
|
||||
|
||||
// 统计数据
|
||||
QDateTime m_lastFrameTime;
|
||||
int m_frameCount;
|
||||
int m_totalFrameCount;
|
||||
double m_currentFps;
|
||||
// 统计数据 - 深度图
|
||||
QDateTime m_lastDepthFrameTime;
|
||||
int m_depthFrameCount;
|
||||
int m_totalDepthFrameCount;
|
||||
double m_currentDepthFps;
|
||||
|
||||
// 统计数据 - 点云
|
||||
QDateTime m_lastPointCloudFrameTime;
|
||||
int m_pointCloudFrameCount;
|
||||
int m_totalPointCloudFrameCount;
|
||||
double m_currentPointCloudFps;
|
||||
|
||||
// 统计数据 - 左红外
|
||||
QDateTime m_lastLeftFrameTime;
|
||||
int m_leftFrameCount;
|
||||
int m_totalLeftFrameCount;
|
||||
double m_currentLeftFps;
|
||||
|
||||
// 统计数据 - 右红外
|
||||
QDateTime m_lastRightFrameTime;
|
||||
int m_rightFrameCount;
|
||||
int m_totalRightFrameCount;
|
||||
double m_currentRightFps;
|
||||
|
||||
// 统计数据 - RGB
|
||||
QDateTime m_lastRgbFrameTime;
|
||||
int m_rgbFrameCount;
|
||||
int m_totalRgbFrameCount;
|
||||
double m_currentRgbFps;
|
||||
|
||||
// 解码处理标志(防止线程积压导致闪烁)
|
||||
QAtomicInt m_rgbProcessing;
|
||||
QAtomicInt m_leftIRProcessing;
|
||||
QAtomicInt m_rightIRProcessing;
|
||||
QAtomicInt m_pointCloudProcessing;
|
||||
QAtomicInt m_pointCloudDropCounter;
|
||||
bool m_leftIrDisplayRangeInited;
|
||||
float m_leftIrDisplayMin;
|
||||
float m_leftIrDisplayMax;
|
||||
bool m_rightIrDisplayRangeInited;
|
||||
float m_rightIrDisplayMin;
|
||||
float m_rightIrDisplayMax;
|
||||
int m_rgbSkipCounter; // RGB帧跳过计数器
|
||||
|
||||
// 相机启用状态标志(防止关闭后闪烁)
|
||||
QAtomicInt m_leftIREnabled;
|
||||
QAtomicInt m_rightIREnabled;
|
||||
QAtomicInt m_rgbEnabled;
|
||||
};
|
||||
|
||||
#endif // MAINWINDOW_H
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
void MainWindow::onDataReceived(const QByteArray &data)
|
||||
{
|
||||
static int packetCount = 0;
|
||||
packetCount++;
|
||||
|
||||
if (packetCount % 100 == 0) {
|
||||
qDebug() << "Received" << packetCount << "packets, latest size:" << data.size() << "bytes";
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "gui/PointCloudGLWidget.h"
|
||||
#include <QDebug>
|
||||
#include <QPushButton>
|
||||
#include <cmath>
|
||||
#include <cfloat>
|
||||
|
||||
@@ -9,14 +10,32 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget *parent)
|
||||
, m_vertexBuffer(nullptr)
|
||||
, m_vao(nullptr)
|
||||
, m_pointCount(0)
|
||||
, m_orthoSize(2000.0f) // 正交投影视野大小
|
||||
, m_minZ(0.0f)
|
||||
, m_maxZ(1000.0f)
|
||||
, m_fov(60.0f) // 透视投影视场角
|
||||
, m_rotationX(0.0f) // 从正面看(0度)
|
||||
, m_rotationY(0.0f) // 从正面看(0度)
|
||||
, m_translation(0.0f, 0.0f, 0.0f)
|
||||
, m_rotationY(0.0f) // 不旋转Y轴
|
||||
, m_cloudCenter(0.0f, 0.0f, 0.0f)
|
||||
, m_viewDistance(1000.0f)
|
||||
, m_panOffset(0.0f, 0.0f, 0.0f)
|
||||
, m_zoom(1.0f) // 缩放因子
|
||||
, m_leftButtonPressed(false)
|
||||
, m_rightButtonPressed(false)
|
||||
, m_firstFrame(true)
|
||||
, m_colorMode(0) // 默认黑白模式
|
||||
{
|
||||
setMinimumSize(400, 400);
|
||||
|
||||
// 添加重置视角按钮
|
||||
QPushButton *resetBtn = new QPushButton("重置", this);
|
||||
resetBtn->setFixedSize(60, 30);
|
||||
resetBtn->move(10, 10);
|
||||
resetBtn->setStyleSheet(
|
||||
"QPushButton { background-color: rgba(50, 50, 50, 180); color: white; border: 1px solid #555; border-radius: 4px; }"
|
||||
"QPushButton:hover { background-color: rgba(70, 70, 70, 200); }"
|
||||
"QPushButton:pressed { background-color: rgba(40, 40, 40, 220); }"
|
||||
);
|
||||
connect(resetBtn, &QPushButton::clicked, this, &PointCloudGLWidget::resetView);
|
||||
}
|
||||
|
||||
PointCloudGLWidget::~PointCloudGLWidget()
|
||||
@@ -40,7 +59,7 @@ void PointCloudGLWidget::initializeGL()
|
||||
{
|
||||
initializeOpenGLFunctions();
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClearColor(0.1f, 0.1f, 0.15f, 1.0f); // 深灰色背景
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
@@ -65,18 +84,39 @@ void PointCloudGLWidget::setupShaders()
|
||||
#version 330 core
|
||||
layout(location = 0) in vec3 position;
|
||||
uniform mat4 mvp;
|
||||
uniform float minZ;
|
||||
uniform float maxZ;
|
||||
out float depth;
|
||||
void main() {
|
||||
gl_Position = mvp * vec4(position, 1.0);
|
||||
gl_PointSize = 2.0;
|
||||
gl_PointSize = 1.0;
|
||||
depth = (position.z - minZ) / (maxZ - minZ);
|
||||
}
|
||||
)";
|
||||
|
||||
// 片段着色器
|
||||
// 片段着色器 - 支持黑白和彩色两种模式
|
||||
const char *fragmentShaderSource = R"(
|
||||
#version 330 core
|
||||
in float depth;
|
||||
uniform int colorMode;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
float d = clamp(depth, 0.0, 1.0);
|
||||
if (colorMode == 0) {
|
||||
fragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
} else {
|
||||
vec3 color;
|
||||
if (d < 0.25) {
|
||||
color = mix(vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 1.0), d * 4.0);
|
||||
} else if (d < 0.5) {
|
||||
color = mix(vec3(0.0, 1.0, 1.0), vec3(0.0, 1.0, 0.0), (d - 0.25) * 4.0);
|
||||
} else if (d < 0.75) {
|
||||
color = mix(vec3(0.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), (d - 0.5) * 4.0);
|
||||
} else {
|
||||
color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), (d - 0.75) * 4.0);
|
||||
}
|
||||
fragColor = vec4(color, 1.0);
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
@@ -97,11 +137,12 @@ void PointCloudGLWidget::resizeGL(int w, int h)
|
||||
{
|
||||
m_projection.setToIdentity();
|
||||
|
||||
// 使用正交投影代替透视投影,避免"喷射状"效果
|
||||
// 使用正交投影,避免透视变形
|
||||
float aspect = float(w) / float(h);
|
||||
m_projection.ortho(-m_orthoSize * aspect, m_orthoSize * aspect,
|
||||
-m_orthoSize, m_orthoSize,
|
||||
-50000.0f, 50000.0f); // 近平面和远平面
|
||||
float orthoSize = m_viewDistance * 0.5f / m_zoom;
|
||||
m_projection.ortho(-orthoSize * aspect, orthoSize * aspect,
|
||||
-orthoSize, orthoSize,
|
||||
-50000.0f, 50000.0f);
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::paintGL()
|
||||
@@ -112,18 +153,25 @@ void PointCloudGLWidget::paintGL()
|
||||
return;
|
||||
}
|
||||
|
||||
// 每帧重新计算正交投影矩阵(确保使用最新的m_orthoSize)
|
||||
// 重新计算正交投影矩阵
|
||||
m_projection.setToIdentity();
|
||||
float aspect = float(width()) / float(height());
|
||||
m_projection.ortho(-m_orthoSize * aspect, m_orthoSize * aspect,
|
||||
-m_orthoSize, m_orthoSize,
|
||||
float orthoSize = m_viewDistance * 0.5f / m_zoom;
|
||||
m_projection.ortho(-orthoSize * aspect, orthoSize * aspect,
|
||||
-orthoSize, orthoSize,
|
||||
-50000.0f, 50000.0f);
|
||||
|
||||
// 设置view矩阵
|
||||
// 设置view矩阵 - 轨道相机模式(围绕点云中心旋转)
|
||||
m_view.setToIdentity();
|
||||
// 1. 用户平移偏移
|
||||
m_view.translate(m_panOffset);
|
||||
// 2. 相机后退到观察距离
|
||||
m_view.translate(0.0f, 0.0f, -m_viewDistance);
|
||||
// 3. 应用旋转(围绕原点,即点云中心)
|
||||
m_view.rotate(m_rotationX, 1.0f, 0.0f, 0.0f);
|
||||
m_view.rotate(m_rotationY, 0.0f, 1.0f, 0.0f);
|
||||
m_view.translate(m_translation);
|
||||
// 4. 将点云中心移到原点
|
||||
m_view.translate(-m_cloudCenter);
|
||||
|
||||
// 设置model矩阵
|
||||
m_model.setToIdentity();
|
||||
@@ -134,6 +182,9 @@ void PointCloudGLWidget::paintGL()
|
||||
// 绑定shader和设置uniform
|
||||
m_program->bind();
|
||||
m_program->setUniformValue("mvp", mvp);
|
||||
m_program->setUniformValue("minZ", m_minZ);
|
||||
m_program->setUniformValue("maxZ", m_maxZ);
|
||||
m_program->setUniformValue("colorMode", m_colorMode);
|
||||
|
||||
// 绑定VAO和绘制
|
||||
m_vao->bind();
|
||||
@@ -164,10 +215,10 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent *event)
|
||||
m_rotationY += delta.x() * 0.5f;
|
||||
update();
|
||||
} else if (m_rightButtonPressed) {
|
||||
// 右键:平移(根据正交投影视野大小调整平移速度)
|
||||
float scale = m_orthoSize * 0.002f;
|
||||
m_translation.setX(m_translation.x() + delta.x() * scale);
|
||||
m_translation.setY(m_translation.y() - delta.y() * scale);
|
||||
// 右键:平移(根据观察距离调整平移速度)
|
||||
float scale = m_viewDistance * 0.002f;
|
||||
m_panOffset.setX(m_panOffset.x() + delta.x() * scale);
|
||||
m_panOffset.setY(m_panOffset.y() - delta.y() * scale);
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -183,12 +234,12 @@ void PointCloudGLWidget::mouseReleaseEvent(QMouseEvent *event)
|
||||
|
||||
void PointCloudGLWidget::wheelEvent(QWheelEvent *event)
|
||||
{
|
||||
// 滚轮:缩放(调整正交投影视野大小)
|
||||
// 滚轮:缩放(调整zoom因子)
|
||||
float delta = event->angleDelta().y() / 120.0f;
|
||||
m_orthoSize *= (1.0f - delta * 0.1f);
|
||||
m_orthoSize = qMax(100.0f, qMin(m_orthoSize, 10000.0f)); // 范围:100-10000
|
||||
m_zoom *= (1.0f + delta * 0.1f);
|
||||
m_zoom = qMax(0.1f, qMin(m_zoom, 10.0f)); // 范围:0.1-10倍
|
||||
|
||||
update(); // 触发重绘,paintGL会使用新的m_orthoSize
|
||||
update();
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud)
|
||||
@@ -197,45 +248,87 @@ void PointCloudGLWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr cl
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤全零点并转换为顶点数组
|
||||
// 过滤全零点并转换为顶点数组,同时计算包围盒
|
||||
m_vertices.clear();
|
||||
|
||||
float minX = FLT_MAX, maxX = -FLT_MAX;
|
||||
float minY = FLT_MAX, maxY = -FLT_MAX;
|
||||
float minZ = FLT_MAX, maxZ = -FLT_MAX;
|
||||
|
||||
for (const auto& point : cloud->points) {
|
||||
if (point.z > 0.01f) {
|
||||
if (point.z > 0.01f) { // 过滤掉无效的零点
|
||||
const float displayZ = -point.z; // Flip front/back axis for viewer convention.
|
||||
m_vertices.push_back(point.x);
|
||||
m_vertices.push_back(point.y);
|
||||
m_vertices.push_back(point.z);
|
||||
m_vertices.push_back(-point.y);
|
||||
m_vertices.push_back(displayZ);
|
||||
|
||||
// 统计坐标范围
|
||||
// 更新包围盒
|
||||
if (point.x < minX) minX = point.x;
|
||||
if (point.x > maxX) maxX = point.x;
|
||||
if (point.y < minY) minY = point.y;
|
||||
if (point.y > maxY) maxY = point.y;
|
||||
if (point.z < minZ) minZ = point.z;
|
||||
if (point.z > maxZ) maxZ = point.z;
|
||||
if (displayZ < minZ) minZ = displayZ;
|
||||
if (displayZ > maxZ) maxZ = displayZ;
|
||||
}
|
||||
}
|
||||
|
||||
m_pointCount = m_vertices.size() / 3;
|
||||
|
||||
// 添加调试日志
|
||||
static int updateCount = 0;
|
||||
if (updateCount < 3 || updateCount % 100 == 0) {
|
||||
qDebug() << "[PointCloudGLWidget] Update" << updateCount << "- Points:" << m_pointCount
|
||||
<< "Total cloud size:" << cloud->size();
|
||||
qDebug() << " X range:" << minX << "to" << maxX;
|
||||
qDebug() << " Y range:" << minY << "to" << maxY;
|
||||
qDebug() << " Z range:" << minZ << "to" << maxZ;
|
||||
// 保存深度范围用于着色
|
||||
if (m_pointCount > 0) {
|
||||
m_minZ = minZ;
|
||||
m_maxZ = maxZ;
|
||||
}
|
||||
|
||||
// 只在首帧时自动调整相机位置,之后保持用户交互状态
|
||||
if (m_pointCount > 0 && m_firstFrame) {
|
||||
// 计算点云中心
|
||||
float centerX = (minX + maxX) / 2.0f;
|
||||
float centerY = (minY + maxY) / 2.0f;
|
||||
float centerZ = (minZ + maxZ) / 2.0f;
|
||||
|
||||
// 计算点云尺寸
|
||||
float depthRange = maxZ - minZ;
|
||||
float sizeX = maxX - minX;
|
||||
float sizeY = maxY - minY;
|
||||
float maxSize = std::max({sizeX, sizeY, depthRange});
|
||||
|
||||
// 设置点云中心
|
||||
m_cloudCenter = QVector3D(centerX, centerY, centerZ);
|
||||
|
||||
// 计算观察距离,让相机从外部观察点云
|
||||
m_viewDistance = maxSize * 1.5f;
|
||||
|
||||
// 重置平移偏移和旋转角度
|
||||
m_panOffset = QVector3D(0.0f, 0.0f, 0.0f);
|
||||
m_rotationX = 0.0f;
|
||||
m_rotationY = 0.0f;
|
||||
|
||||
// 设置缩放
|
||||
m_zoom = 1.0f;
|
||||
|
||||
qDebug() << "[PointCloudGLWidget] 首帧自动居中 - 点云中心:" << centerX << centerY << centerZ
|
||||
<< "观察距离:" << m_viewDistance;
|
||||
m_firstFrame = false; // 标记首帧已处理
|
||||
}
|
||||
updateCount++;
|
||||
|
||||
updateBuffers();
|
||||
update();
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::resetView()
|
||||
{
|
||||
// 重置所有视角参数到初始状态
|
||||
m_rotationX = 0.0f;
|
||||
m_rotationY = 0.0f;
|
||||
m_panOffset = QVector3D(0.0f, 0.0f, 0.0f);
|
||||
m_zoom = 1.0f;
|
||||
m_firstFrame = true; // 标记为首帧,下次更新时会重新计算视角
|
||||
|
||||
update();
|
||||
qDebug() << "[PointCloudGLWidget] 视角已重置";
|
||||
}
|
||||
|
||||
void PointCloudGLWidget::updateBuffers()
|
||||
{
|
||||
if (m_vertices.empty() || !m_vao || !m_vertexBuffer) {
|
||||
|
||||
@@ -54,3 +54,15 @@ void PointCloudWidget::updatePointCloud(pcl::PointCloud<pcl::PointXYZ>::Ptr clou
|
||||
// 更新OpenGL显示
|
||||
m_glWidget->updatePointCloud(cloud);
|
||||
}
|
||||
|
||||
void PointCloudWidget::setColorMode(bool enabled)
|
||||
{
|
||||
if (m_glWidget) {
|
||||
m_glWidget->setColorMode(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
bool PointCloudWidget::colorMode() const
|
||||
{
|
||||
return m_glWidget ? m_glWidget->colorMode() : false;
|
||||
}
|
||||
|
||||
32
src/main.cpp
32
src/main.cpp
@@ -1,12 +1,15 @@
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "gui/MainWindow.h"
|
||||
#include "core/Logger.h"
|
||||
|
||||
// Custom message handler to redirect qDebug output to Logger
|
||||
// Redirect Qt log output to file logger.
|
||||
void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
|
||||
{
|
||||
Q_UNUSED(context);
|
||||
Logger *logger = Logger::instance();
|
||||
|
||||
switch (type) {
|
||||
@@ -30,29 +33,32 @@ int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置应用程序信息
|
||||
app.setOrganizationName("UpperControl");
|
||||
app.setApplicationName("UpperControl GUI");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("Viewer");
|
||||
app.setApplicationName("Viewer");
|
||||
app.setApplicationVersion("0.3.3");
|
||||
|
||||
// 初始化Logger(在可执行文件同目录下)
|
||||
QString logPath = QCoreApplication::applicationDirPath() + "/d330viewer.log";
|
||||
// Prefer LocalAppData so MSI installs under Program Files can always write logs.
|
||||
QString logDir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||
if (logDir.isEmpty()) {
|
||||
logDir = QCoreApplication::applicationDirPath();
|
||||
}
|
||||
QDir().mkpath(logDir);
|
||||
|
||||
const QString logPath = QDir(logDir).filePath("viewer.log");
|
||||
Logger::instance()->setLogFile(logPath);
|
||||
Logger::instance()->setMaxLines(10000); // 保留最新10000行
|
||||
Logger::instance()->setMaxLines(10000);
|
||||
|
||||
// 安装消息处理器
|
||||
qInstallMessageHandler(messageHandler);
|
||||
|
||||
qDebug() << "D330Viewer started";
|
||||
qDebug() << "Viewer started";
|
||||
qDebug() << "Log file:" << logPath;
|
||||
|
||||
// 创建并显示主窗口
|
||||
MainWindow mainWindow;
|
||||
mainWindow.show();
|
||||
|
||||
int result = app.exec();
|
||||
const int result = app.exec();
|
||||
|
||||
qDebug() << "D330Viewer exiting";
|
||||
qDebug() << "Viewer exiting";
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user