style:调整文件结束符
This commit is contained in:
parent
2ee6bcd414
commit
7b84fffd2a
@ -1,37 +1,37 @@
|
||||
# CloudUtils搴撻」鐩厤缃枃浠?
|
||||
|
||||
# 鎸囧畾Qt妯″潡
|
||||
QT += core
|
||||
|
||||
# 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
}
|
||||
|
||||
|
||||
# 鎸囧畾C++鏍囧噯
|
||||
CONFIG += c++11
|
||||
|
||||
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/_Inc
|
||||
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc
|
||||
|
||||
INCLUDEPATH += $$PWD/../VrCommon/Inc
|
||||
INCLUDEPATH += $$PWD/../VrUtils/Inc
|
||||
|
||||
# 鎸囧畾鍖呭惈璺緞
|
||||
INCLUDEPATH += ./Inc \
|
||||
./Src
|
||||
|
||||
# 鎸囧畾澶存枃浠?
|
||||
HEADERS += Inc/LaserDataLoader.h \
|
||||
Inc/PointCloudImageUtils.h \
|
||||
Inc/PointCloudFilter.h \
|
||||
Inc/PointCloudXYZRange.h
|
||||
|
||||
# 鎸囧畾婧愭枃浠?
|
||||
SOURCES += Src/LaserDataLoader.cpp \
|
||||
Src/PointCloudImageUtils.cpp \
|
||||
Src/PointCloudFilter.cpp
|
||||
# CloudUtils搴撻」鐩厤缃枃浠?
|
||||
|
||||
# 鎸囧畾Qt妯″潡
|
||||
QT += core
|
||||
|
||||
# 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
}
|
||||
|
||||
|
||||
# 鎸囧畾C++鏍囧噯
|
||||
CONFIG += c++11
|
||||
|
||||
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/_Inc
|
||||
INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc
|
||||
|
||||
INCLUDEPATH += $$PWD/../VrCommon/Inc
|
||||
INCLUDEPATH += $$PWD/../VrUtils/Inc
|
||||
|
||||
# 鎸囧畾鍖呭惈璺緞
|
||||
INCLUDEPATH += ./Inc \
|
||||
./Src
|
||||
|
||||
# 鎸囧畾澶存枃浠?
|
||||
HEADERS += Inc/LaserDataLoader.h \
|
||||
Inc/PointCloudImageUtils.h \
|
||||
Inc/PointCloudFilter.h \
|
||||
Inc/PointCloudXYZRange.h
|
||||
|
||||
# 鎸囧畾婧愭枃浠?
|
||||
SOURCES += Src/LaserDataLoader.cpp \
|
||||
Src/PointCloudImageUtils.cpp \
|
||||
Src/PointCloudFilter.cpp
|
||||
|
||||
@ -1,59 +1,59 @@
|
||||
#ifndef POINTCLOUDFILTER_H
|
||||
#define POINTCLOUDFILTER_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "VZNL_Types.h"
|
||||
|
||||
// Selects the outlier removal algorithm.
|
||||
enum class PointCloudFilterMethod
|
||||
{
|
||||
None = 0,
|
||||
ScanLineContinuity,
|
||||
RadiusOutlier,
|
||||
StatisticalOutlier
|
||||
};
|
||||
|
||||
struct PointCloudFilterParams
|
||||
{
|
||||
PointCloudFilterMethod method = PointCloudFilterMethod::None;
|
||||
|
||||
// ScanLineContinuity parameters. Units follow the point cloud (normally mm).
|
||||
double continuityThreshold = 20.0;
|
||||
std::size_t minSegmentLength = 5;
|
||||
|
||||
// RadiusOutlier parameters. minNeighbors excludes the point itself.
|
||||
double radius = 5.0;
|
||||
std::size_t minNeighbors = 2;
|
||||
|
||||
// StatisticalOutlier parameters.
|
||||
std::size_t meanK = 8;
|
||||
double standardDeviationMultiplier = 1.0;
|
||||
// Zero estimates a radius from the cloud density; non-zero limits the search.
|
||||
double searchRadius = 0.0;
|
||||
};
|
||||
|
||||
struct PointCloudFilterStats
|
||||
{
|
||||
std::size_t inputPointCount = 0;
|
||||
std::size_t outputPointCount = 0;
|
||||
std::size_t removedPointCount = 0;
|
||||
};
|
||||
|
||||
class PointCloudFilter
|
||||
{
|
||||
public:
|
||||
// The input is not modified. Original nPointIdx values are retained.
|
||||
static bool Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
|
||||
std::vector<std::vector<SVzNL3DPosition>>& output,
|
||||
const PointCloudFilterParams& params = PointCloudFilterParams(),
|
||||
PointCloudFilterStats* stats = nullptr);
|
||||
|
||||
static bool Filter(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params = PointCloudFilterParams(),
|
||||
PointCloudFilterStats* stats = nullptr);
|
||||
};
|
||||
|
||||
#endif // POINTCLOUDFILTER_H
|
||||
#ifndef POINTCLOUDFILTER_H
|
||||
#define POINTCLOUDFILTER_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
#include "VZNL_Types.h"
|
||||
|
||||
// Selects the outlier removal algorithm.
|
||||
enum class PointCloudFilterMethod
|
||||
{
|
||||
None = 0,
|
||||
ScanLineContinuity,
|
||||
RadiusOutlier,
|
||||
StatisticalOutlier
|
||||
};
|
||||
|
||||
struct PointCloudFilterParams
|
||||
{
|
||||
PointCloudFilterMethod method = PointCloudFilterMethod::None;
|
||||
|
||||
// ScanLineContinuity parameters. Units follow the point cloud (normally mm).
|
||||
double continuityThreshold = 20.0;
|
||||
std::size_t minSegmentLength = 5;
|
||||
|
||||
// RadiusOutlier parameters. minNeighbors excludes the point itself.
|
||||
double radius = 5.0;
|
||||
std::size_t minNeighbors = 2;
|
||||
|
||||
// StatisticalOutlier parameters.
|
||||
std::size_t meanK = 8;
|
||||
double standardDeviationMultiplier = 1.0;
|
||||
// Zero estimates a radius from the cloud density; non-zero limits the search.
|
||||
double searchRadius = 0.0;
|
||||
};
|
||||
|
||||
struct PointCloudFilterStats
|
||||
{
|
||||
std::size_t inputPointCount = 0;
|
||||
std::size_t outputPointCount = 0;
|
||||
std::size_t removedPointCount = 0;
|
||||
};
|
||||
|
||||
class PointCloudFilter
|
||||
{
|
||||
public:
|
||||
// The input is not modified. Original nPointIdx values are retained.
|
||||
static bool Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
|
||||
std::vector<std::vector<SVzNL3DPosition>>& output,
|
||||
const PointCloudFilterParams& params = PointCloudFilterParams(),
|
||||
PointCloudFilterStats* stats = nullptr);
|
||||
|
||||
static bool Filter(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params = PointCloudFilterParams(),
|
||||
PointCloudFilterStats* stats = nullptr);
|
||||
};
|
||||
|
||||
#endif // POINTCLOUDFILTER_H
|
||||
|
||||
@ -1,72 +1,72 @@
|
||||
#ifndef POINT_CLOUD_XYZ_RANGE_H
|
||||
#define POINT_CLOUD_XYZ_RANGE_H
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
// XYZ 范围外点的处理方式。
|
||||
//
|
||||
// ReplaceWithZero 保留点在线内的原始槽位,并仅把三维坐标置为零;
|
||||
// Remove 则删除范围外点并压缩扫描线。保留槽位对于按扫描线/网格
|
||||
// 处理的调用方很重要,因此它也是启用范围过滤时的默认模式。
|
||||
enum class PointCloudXYZRangeFilterMode
|
||||
{
|
||||
ReplaceWithZero = 0,
|
||||
Remove = 1
|
||||
};
|
||||
|
||||
// 兼容不需要区分 XYZ 范围限定名称的调用方。
|
||||
using PointCloudRangeFilterMode = PointCloudXYZRangeFilterMode;
|
||||
|
||||
// 点云加载使用的可选轴对齐范围;默认关闭以保持原有读取行为。
|
||||
struct PointCloudXYZRange
|
||||
{
|
||||
using FilterMode = PointCloudXYZRangeFilterMode;
|
||||
|
||||
bool enabled = false;
|
||||
double minX = -std::numeric_limits<double>::infinity();
|
||||
double maxX = std::numeric_limits<double>::infinity();
|
||||
double minY = -std::numeric_limits<double>::infinity();
|
||||
double maxY = std::numeric_limits<double>::infinity();
|
||||
double minZ = -std::numeric_limits<double>::infinity();
|
||||
double maxZ = std::numeric_limits<double>::infinity();
|
||||
// Keep this after the legacy bounds so aggregate initialization remains compatible.
|
||||
PointCloudXYZRangeFilterMode filterMode =
|
||||
PointCloudXYZRangeFilterMode::ReplaceWithZero;
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return !std::isnan(minX) && !std::isnan(maxX)
|
||||
&& !std::isnan(minY) && !std::isnan(maxY)
|
||||
&& !std::isnan(minZ) && !std::isnan(maxZ)
|
||||
&& minX <= maxX && minY <= maxY && minZ <= maxZ;
|
||||
}
|
||||
|
||||
bool contains(double x, double y, double z) const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!isValid() || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return x >= minX && x <= maxX
|
||||
&& y >= minY && y <= maxY
|
||||
&& z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
bool isEnabled() const
|
||||
{
|
||||
return enabled && isValid();
|
||||
}
|
||||
};
|
||||
|
||||
using PointCloudRange = PointCloudXYZRange;
|
||||
|
||||
#endif // POINT_CLOUD_XYZ_RANGE_H
|
||||
#ifndef POINT_CLOUD_XYZ_RANGE_H
|
||||
#define POINT_CLOUD_XYZ_RANGE_H
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
// XYZ 范围外点的处理方式。
|
||||
//
|
||||
// ReplaceWithZero 保留点在线内的原始槽位,并仅把三维坐标置为零;
|
||||
// Remove 则删除范围外点并压缩扫描线。保留槽位对于按扫描线/网格
|
||||
// 处理的调用方很重要,因此它也是启用范围过滤时的默认模式。
|
||||
enum class PointCloudXYZRangeFilterMode
|
||||
{
|
||||
ReplaceWithZero = 0,
|
||||
Remove = 1
|
||||
};
|
||||
|
||||
// 兼容不需要区分 XYZ 范围限定名称的调用方。
|
||||
using PointCloudRangeFilterMode = PointCloudXYZRangeFilterMode;
|
||||
|
||||
// 点云加载使用的可选轴对齐范围;默认关闭以保持原有读取行为。
|
||||
struct PointCloudXYZRange
|
||||
{
|
||||
using FilterMode = PointCloudXYZRangeFilterMode;
|
||||
|
||||
bool enabled = false;
|
||||
double minX = -std::numeric_limits<double>::infinity();
|
||||
double maxX = std::numeric_limits<double>::infinity();
|
||||
double minY = -std::numeric_limits<double>::infinity();
|
||||
double maxY = std::numeric_limits<double>::infinity();
|
||||
double minZ = -std::numeric_limits<double>::infinity();
|
||||
double maxZ = std::numeric_limits<double>::infinity();
|
||||
// Keep this after the legacy bounds so aggregate initialization remains compatible.
|
||||
PointCloudXYZRangeFilterMode filterMode =
|
||||
PointCloudXYZRangeFilterMode::ReplaceWithZero;
|
||||
|
||||
bool isValid() const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return !std::isnan(minX) && !std::isnan(maxX)
|
||||
&& !std::isnan(minY) && !std::isnan(maxY)
|
||||
&& !std::isnan(minZ) && !std::isnan(maxZ)
|
||||
&& minX <= maxX && minY <= maxY && minZ <= maxZ;
|
||||
}
|
||||
|
||||
bool contains(double x, double y, double z) const
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (!isValid() || !std::isfinite(x) || !std::isfinite(y) || !std::isfinite(z))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return x >= minX && x <= maxX
|
||||
&& y >= minY && y <= maxY
|
||||
&& z >= minZ && z <= maxZ;
|
||||
}
|
||||
|
||||
bool isEnabled() const
|
||||
{
|
||||
return enabled && isValid();
|
||||
}
|
||||
};
|
||||
|
||||
using PointCloudRange = PointCloudXYZRange;
|
||||
|
||||
#endif // POINT_CLOUD_XYZ_RANGE_H
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,383 +1,383 @@
|
||||
#include "PointCloudFilter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct GridKey
|
||||
{
|
||||
long long x;
|
||||
long long y;
|
||||
long long z;
|
||||
|
||||
bool operator==(const GridKey& other) const
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
};
|
||||
|
||||
struct GridKeyHash
|
||||
{
|
||||
std::size_t operator()(const GridKey& key) const
|
||||
{
|
||||
const std::size_t h1 = std::hash<long long>()(key.x);
|
||||
const std::size_t h2 = std::hash<long long>()(key.y);
|
||||
const std::size_t h3 = std::hash<long long>()(key.z);
|
||||
return h1 ^ (h2 + static_cast<std::size_t>(0x9e3779b9) + (h1 << 6) + (h1 >> 2))
|
||||
^ (h3 + static_cast<std::size_t>(0x9e3779b9) + (h2 << 6) + (h2 >> 2));
|
||||
}
|
||||
};
|
||||
|
||||
struct FlatPoint
|
||||
{
|
||||
SVzNL3DPosition point;
|
||||
};
|
||||
|
||||
bool IsFinite(const SVzNL3DPosition& point)
|
||||
{
|
||||
return std::isfinite(point.pt3D.x) && std::isfinite(point.pt3D.y)
|
||||
&& std::isfinite(point.pt3D.z);
|
||||
}
|
||||
|
||||
double SquaredDistance(const SVzNL3DPosition& first, const SVzNL3DPosition& second)
|
||||
{
|
||||
const double dx = first.pt3D.x - second.pt3D.x;
|
||||
const double dy = first.pt3D.y - second.pt3D.y;
|
||||
const double dz = first.pt3D.z - second.pt3D.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
long long ToCell(double value, double cellSize)
|
||||
{
|
||||
const double coordinate = std::floor(value / cellSize);
|
||||
if (coordinate <= static_cast<double>(std::numeric_limits<long long>::min())) {
|
||||
return std::numeric_limits<long long>::min();
|
||||
}
|
||||
if (coordinate >= static_cast<double>(std::numeric_limits<long long>::max())) {
|
||||
return std::numeric_limits<long long>::max();
|
||||
}
|
||||
return static_cast<long long>(coordinate);
|
||||
}
|
||||
|
||||
GridKey GetKey(const SVzNL3DPosition& point, double cellSize)
|
||||
{
|
||||
GridKey key;
|
||||
key.x = ToCell(point.pt3D.x, cellSize);
|
||||
key.y = ToCell(point.pt3D.y, cellSize);
|
||||
key.z = ToCell(point.pt3D.z, cellSize);
|
||||
return key;
|
||||
}
|
||||
|
||||
void ResetStats(PointCloudFilterStats* stats, std::size_t inputCount)
|
||||
{
|
||||
if (stats != nullptr) {
|
||||
stats->inputPointCount = inputCount;
|
||||
stats->outputPointCount = 0;
|
||||
stats->removedPointCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void FinishStats(PointCloudFilterStats* stats, std::size_t outputCount)
|
||||
{
|
||||
if (stats != nullptr) {
|
||||
stats->outputPointCount = outputCount;
|
||||
stats->removedPointCount = stats->inputPointCount >= outputCount
|
||||
? stats->inputPointCount - outputCount : 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool FilterScanLine(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params)
|
||||
{
|
||||
output.clear();
|
||||
if (input.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t minLength = std::max<std::size_t>(1, params.minSegmentLength);
|
||||
std::size_t segmentBegin = 0;
|
||||
for (std::size_t index = 1; index <= input.size(); ++index) {
|
||||
bool connected = index < input.size() && IsFinite(input[index - 1])
|
||||
&& IsFinite(input[index])
|
||||
&& std::abs(input[index].pt3D.z - input[index - 1].pt3D.z)
|
||||
<= std::max(0.0, params.continuityThreshold);
|
||||
if (index == input.size() || !connected) {
|
||||
if (index - segmentBegin >= minLength) {
|
||||
output.insert(output.end(), input.begin() + static_cast<std::ptrdiff_t>(segmentBegin),
|
||||
input.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
segmentBegin = index;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double EstimateCellSize(const std::vector<FlatPoint>& points)
|
||||
{
|
||||
if (points.size() < 2) {
|
||||
return 1.0;
|
||||
}
|
||||
double minX = points[0].point.pt3D.x;
|
||||
double maxX = minX;
|
||||
double minY = points[0].point.pt3D.y;
|
||||
double maxY = minY;
|
||||
double minZ = points[0].point.pt3D.z;
|
||||
double maxZ = minZ;
|
||||
for (std::size_t index = 1; index < points.size(); ++index) {
|
||||
const SVzNL3DPoint& value = points[index].point.pt3D;
|
||||
minX = std::min(minX, value.x);
|
||||
maxX = std::max(maxX, value.x);
|
||||
minY = std::min(minY, value.y);
|
||||
maxY = std::max(maxY, value.y);
|
||||
minZ = std::min(minZ, value.z);
|
||||
maxZ = std::max(maxZ, value.z);
|
||||
}
|
||||
const double diagonal = std::sqrt((maxX - minX) * (maxX - minX)
|
||||
+ (maxY - minY) * (maxY - minY) + (maxZ - minZ) * (maxZ - minZ));
|
||||
const double scale = diagonal / std::cbrt(static_cast<double>(points.size()));
|
||||
return std::max(scale, 1e-6);
|
||||
}
|
||||
|
||||
template <typename KeepPredicate>
|
||||
void FilterWithGrid(const std::vector<FlatPoint>& points, double cellSize,
|
||||
double radius, KeepPredicate keepPoint,
|
||||
std::vector<bool>& keep)
|
||||
{
|
||||
std::unordered_map<GridKey, std::vector<std::size_t>, GridKeyHash> grid;
|
||||
grid.reserve(points.size() * 2 + 1);
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
grid[GetKey(points[index].point, cellSize)].push_back(index);
|
||||
}
|
||||
|
||||
const long long cellRange = std::max<long long>(1, static_cast<long long>(std::ceil(radius / cellSize)));
|
||||
const double radiusSquared = radius * radius;
|
||||
std::vector<std::size_t> candidates;
|
||||
candidates.reserve(32);
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
const GridKey center = GetKey(points[index].point, cellSize);
|
||||
candidates.clear();
|
||||
for (long long x = -cellRange; x <= cellRange; ++x) {
|
||||
for (long long y = -cellRange; y <= cellRange; ++y) {
|
||||
for (long long z = -cellRange; z <= cellRange; ++z) {
|
||||
GridKey key = {center.x + x, center.y + y, center.z + z};
|
||||
const auto found = grid.find(key);
|
||||
if (found != grid.end()) {
|
||||
candidates.insert(candidates.end(), found->second.begin(), found->second.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keep[index] = keepPoint(index, candidates, radiusSquared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PointCloudFilter::Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
|
||||
std::vector<std::vector<SVzNL3DPosition>>& output,
|
||||
const PointCloudFilterParams& params,
|
||||
PointCloudFilterStats* stats)
|
||||
{
|
||||
std::size_t inputCount = 0;
|
||||
for (const auto& line : input) {
|
||||
inputCount += line.size();
|
||||
}
|
||||
ResetStats(stats, inputCount);
|
||||
output.clear();
|
||||
output.resize(input.size());
|
||||
|
||||
if (params.method == PointCloudFilterMethod::None) {
|
||||
output = input;
|
||||
FinishStats(stats, inputCount);
|
||||
return true;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
|
||||
std::size_t outputCount = 0;
|
||||
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
|
||||
FilterScanLine(input[lineIndex], output[lineIndex], params);
|
||||
outputCount += output[lineIndex].size();
|
||||
}
|
||||
FinishStats(stats, outputCount);
|
||||
return true;
|
||||
}
|
||||
if (params.method != PointCloudFilterMethod::RadiusOutlier
|
||||
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<FlatPoint> points;
|
||||
points.reserve(inputCount);
|
||||
for (const auto& line : input) {
|
||||
for (const SVzNL3DPosition& point : line) {
|
||||
if (IsFinite(point)) {
|
||||
points.push_back({point});
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<SVzNL3DPosition> filtered;
|
||||
std::vector<SVzNL3DPosition> flatInput;
|
||||
flatInput.reserve(points.size());
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
flatInput.push_back(points[index].point);
|
||||
// Encode the flattened position only for reconstructing scan lines.
|
||||
flatInput.back().nPointIdx = static_cast<int>(index);
|
||||
}
|
||||
if (!Filter(flatInput, filtered, params, nullptr)) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<bool> retained(points.size(), false);
|
||||
for (const SVzNL3DPosition& point : filtered) {
|
||||
if (point.nPointIdx >= 0
|
||||
&& static_cast<std::size_t>(point.nPointIdx) < retained.size()) {
|
||||
retained[static_cast<std::size_t>(point.nPointIdx)] = true;
|
||||
}
|
||||
}
|
||||
std::size_t pointIndex = 0;
|
||||
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
|
||||
for (const SVzNL3DPosition& point : input[lineIndex]) {
|
||||
if (IsFinite(point)) {
|
||||
if (pointIndex < retained.size() && retained[pointIndex]) {
|
||||
output[lineIndex].push_back(point);
|
||||
}
|
||||
++pointIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::size_t outputCount = 0;
|
||||
for (const auto& line : output) {
|
||||
outputCount += line.size();
|
||||
}
|
||||
FinishStats(stats, outputCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PointCloudFilter::Filter(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params,
|
||||
PointCloudFilterStats* stats)
|
||||
{
|
||||
ResetStats(stats, input.size());
|
||||
output.clear();
|
||||
if (params.method == PointCloudFilterMethod::None) {
|
||||
output = input;
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
|
||||
FilterScanLine(input, output, params);
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
if (params.method != PointCloudFilterMethod::RadiusOutlier
|
||||
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
|
||||
return false;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<FlatPoint> points;
|
||||
points.reserve(input.size());
|
||||
for (const SVzNL3DPosition& point : input) {
|
||||
if (IsFinite(point)) {
|
||||
points.push_back({point});
|
||||
}
|
||||
}
|
||||
if (points.empty()) {
|
||||
FinishStats(stats, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const double requestedRadius = params.method == PointCloudFilterMethod::RadiusOutlier
|
||||
? params.radius : params.searchRadius;
|
||||
const double radius = requestedRadius > 0.0 ? requestedRadius : EstimateCellSize(points) * 2.0;
|
||||
const double cellSize = std::max(radius, 1e-6);
|
||||
std::vector<bool> keep(points.size(), false);
|
||||
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier) {
|
||||
const std::size_t minNeighbors = params.minNeighbors;
|
||||
FilterWithGrid(points, cellSize, radius,
|
||||
[&points, minNeighbors](std::size_t index, const std::vector<std::size_t>& candidates,
|
||||
double radiusSquared) {
|
||||
std::size_t neighbors = 0;
|
||||
for (std::size_t candidate : candidates) {
|
||||
if (candidate != index && SquaredDistance(points[index].point, points[candidate].point) <= radiusSquared) {
|
||||
++neighbors;
|
||||
}
|
||||
}
|
||||
return neighbors >= minNeighbors;
|
||||
}, keep);
|
||||
}
|
||||
|
||||
if (params.method == PointCloudFilterMethod::StatisticalOutlier) {
|
||||
std::vector<double> meanDistances(points.size(), std::numeric_limits<double>::infinity());
|
||||
FilterWithGrid(points, cellSize, radius,
|
||||
[&points, &meanDistances, ¶ms](std::size_t index,
|
||||
const std::vector<std::size_t>& candidates,
|
||||
double radiusSquared) {
|
||||
std::vector<double> distances;
|
||||
distances.reserve(candidates.size());
|
||||
for (std::size_t candidate : candidates) {
|
||||
if (candidate != index) {
|
||||
const double distance = SquaredDistance(points[index].point, points[candidate].point);
|
||||
if (distance <= radiusSquared) {
|
||||
distances.push_back(std::sqrt(distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (distances.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t count = std::min(params.meanK == 0 ? std::size_t(1) : params.meanK,
|
||||
distances.size());
|
||||
if (count < distances.size()) {
|
||||
std::nth_element(distances.begin(),
|
||||
distances.begin() + static_cast<std::ptrdiff_t>(count),
|
||||
distances.end());
|
||||
}
|
||||
meanDistances[index] = std::accumulate(distances.begin(), distances.begin() + static_cast<std::ptrdiff_t>(count), 0.0)
|
||||
/ static_cast<double>(count);
|
||||
return true;
|
||||
}, keep);
|
||||
std::vector<double> validMeans;
|
||||
for (double value : meanDistances) {
|
||||
if (std::isfinite(value)) {
|
||||
validMeans.push_back(value);
|
||||
}
|
||||
}
|
||||
if (!validMeans.empty()) {
|
||||
const double mean = std::accumulate(validMeans.begin(), validMeans.end(), 0.0)
|
||||
/ static_cast<double>(validMeans.size());
|
||||
double variance = 0.0;
|
||||
for (double value : validMeans) {
|
||||
const double delta = value - mean;
|
||||
variance += delta * delta;
|
||||
}
|
||||
const double deviation = std::sqrt(variance / static_cast<double>(validMeans.size()));
|
||||
const double threshold = mean + std::max(0.0, params.standardDeviationMultiplier) * deviation;
|
||||
for (std::size_t index = 0; index < keep.size(); ++index) {
|
||||
keep[index] = std::isfinite(meanDistances[index]) && meanDistances[index] <= threshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.reserve(points.size());
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
if (keep[index]) {
|
||||
output.push_back(points[index].point);
|
||||
}
|
||||
}
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
#include "PointCloudFilter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace
|
||||
{
|
||||
struct GridKey
|
||||
{
|
||||
long long x;
|
||||
long long y;
|
||||
long long z;
|
||||
|
||||
bool operator==(const GridKey& other) const
|
||||
{
|
||||
return x == other.x && y == other.y && z == other.z;
|
||||
}
|
||||
};
|
||||
|
||||
struct GridKeyHash
|
||||
{
|
||||
std::size_t operator()(const GridKey& key) const
|
||||
{
|
||||
const std::size_t h1 = std::hash<long long>()(key.x);
|
||||
const std::size_t h2 = std::hash<long long>()(key.y);
|
||||
const std::size_t h3 = std::hash<long long>()(key.z);
|
||||
return h1 ^ (h2 + static_cast<std::size_t>(0x9e3779b9) + (h1 << 6) + (h1 >> 2))
|
||||
^ (h3 + static_cast<std::size_t>(0x9e3779b9) + (h2 << 6) + (h2 >> 2));
|
||||
}
|
||||
};
|
||||
|
||||
struct FlatPoint
|
||||
{
|
||||
SVzNL3DPosition point;
|
||||
};
|
||||
|
||||
bool IsFinite(const SVzNL3DPosition& point)
|
||||
{
|
||||
return std::isfinite(point.pt3D.x) && std::isfinite(point.pt3D.y)
|
||||
&& std::isfinite(point.pt3D.z);
|
||||
}
|
||||
|
||||
double SquaredDistance(const SVzNL3DPosition& first, const SVzNL3DPosition& second)
|
||||
{
|
||||
const double dx = first.pt3D.x - second.pt3D.x;
|
||||
const double dy = first.pt3D.y - second.pt3D.y;
|
||||
const double dz = first.pt3D.z - second.pt3D.z;
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
long long ToCell(double value, double cellSize)
|
||||
{
|
||||
const double coordinate = std::floor(value / cellSize);
|
||||
if (coordinate <= static_cast<double>(std::numeric_limits<long long>::min())) {
|
||||
return std::numeric_limits<long long>::min();
|
||||
}
|
||||
if (coordinate >= static_cast<double>(std::numeric_limits<long long>::max())) {
|
||||
return std::numeric_limits<long long>::max();
|
||||
}
|
||||
return static_cast<long long>(coordinate);
|
||||
}
|
||||
|
||||
GridKey GetKey(const SVzNL3DPosition& point, double cellSize)
|
||||
{
|
||||
GridKey key;
|
||||
key.x = ToCell(point.pt3D.x, cellSize);
|
||||
key.y = ToCell(point.pt3D.y, cellSize);
|
||||
key.z = ToCell(point.pt3D.z, cellSize);
|
||||
return key;
|
||||
}
|
||||
|
||||
void ResetStats(PointCloudFilterStats* stats, std::size_t inputCount)
|
||||
{
|
||||
if (stats != nullptr) {
|
||||
stats->inputPointCount = inputCount;
|
||||
stats->outputPointCount = 0;
|
||||
stats->removedPointCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void FinishStats(PointCloudFilterStats* stats, std::size_t outputCount)
|
||||
{
|
||||
if (stats != nullptr) {
|
||||
stats->outputPointCount = outputCount;
|
||||
stats->removedPointCount = stats->inputPointCount >= outputCount
|
||||
? stats->inputPointCount - outputCount : 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool FilterScanLine(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params)
|
||||
{
|
||||
output.clear();
|
||||
if (input.empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t minLength = std::max<std::size_t>(1, params.minSegmentLength);
|
||||
std::size_t segmentBegin = 0;
|
||||
for (std::size_t index = 1; index <= input.size(); ++index) {
|
||||
bool connected = index < input.size() && IsFinite(input[index - 1])
|
||||
&& IsFinite(input[index])
|
||||
&& std::abs(input[index].pt3D.z - input[index - 1].pt3D.z)
|
||||
<= std::max(0.0, params.continuityThreshold);
|
||||
if (index == input.size() || !connected) {
|
||||
if (index - segmentBegin >= minLength) {
|
||||
output.insert(output.end(), input.begin() + static_cast<std::ptrdiff_t>(segmentBegin),
|
||||
input.begin() + static_cast<std::ptrdiff_t>(index));
|
||||
}
|
||||
segmentBegin = index;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double EstimateCellSize(const std::vector<FlatPoint>& points)
|
||||
{
|
||||
if (points.size() < 2) {
|
||||
return 1.0;
|
||||
}
|
||||
double minX = points[0].point.pt3D.x;
|
||||
double maxX = minX;
|
||||
double minY = points[0].point.pt3D.y;
|
||||
double maxY = minY;
|
||||
double minZ = points[0].point.pt3D.z;
|
||||
double maxZ = minZ;
|
||||
for (std::size_t index = 1; index < points.size(); ++index) {
|
||||
const SVzNL3DPoint& value = points[index].point.pt3D;
|
||||
minX = std::min(minX, value.x);
|
||||
maxX = std::max(maxX, value.x);
|
||||
minY = std::min(minY, value.y);
|
||||
maxY = std::max(maxY, value.y);
|
||||
minZ = std::min(minZ, value.z);
|
||||
maxZ = std::max(maxZ, value.z);
|
||||
}
|
||||
const double diagonal = std::sqrt((maxX - minX) * (maxX - minX)
|
||||
+ (maxY - minY) * (maxY - minY) + (maxZ - minZ) * (maxZ - minZ));
|
||||
const double scale = diagonal / std::cbrt(static_cast<double>(points.size()));
|
||||
return std::max(scale, 1e-6);
|
||||
}
|
||||
|
||||
template <typename KeepPredicate>
|
||||
void FilterWithGrid(const std::vector<FlatPoint>& points, double cellSize,
|
||||
double radius, KeepPredicate keepPoint,
|
||||
std::vector<bool>& keep)
|
||||
{
|
||||
std::unordered_map<GridKey, std::vector<std::size_t>, GridKeyHash> grid;
|
||||
grid.reserve(points.size() * 2 + 1);
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
grid[GetKey(points[index].point, cellSize)].push_back(index);
|
||||
}
|
||||
|
||||
const long long cellRange = std::max<long long>(1, static_cast<long long>(std::ceil(radius / cellSize)));
|
||||
const double radiusSquared = radius * radius;
|
||||
std::vector<std::size_t> candidates;
|
||||
candidates.reserve(32);
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
const GridKey center = GetKey(points[index].point, cellSize);
|
||||
candidates.clear();
|
||||
for (long long x = -cellRange; x <= cellRange; ++x) {
|
||||
for (long long y = -cellRange; y <= cellRange; ++y) {
|
||||
for (long long z = -cellRange; z <= cellRange; ++z) {
|
||||
GridKey key = {center.x + x, center.y + y, center.z + z};
|
||||
const auto found = grid.find(key);
|
||||
if (found != grid.end()) {
|
||||
candidates.insert(candidates.end(), found->second.begin(), found->second.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keep[index] = keepPoint(index, candidates, radiusSquared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PointCloudFilter::Filter(const std::vector<std::vector<SVzNL3DPosition>>& input,
|
||||
std::vector<std::vector<SVzNL3DPosition>>& output,
|
||||
const PointCloudFilterParams& params,
|
||||
PointCloudFilterStats* stats)
|
||||
{
|
||||
std::size_t inputCount = 0;
|
||||
for (const auto& line : input) {
|
||||
inputCount += line.size();
|
||||
}
|
||||
ResetStats(stats, inputCount);
|
||||
output.clear();
|
||||
output.resize(input.size());
|
||||
|
||||
if (params.method == PointCloudFilterMethod::None) {
|
||||
output = input;
|
||||
FinishStats(stats, inputCount);
|
||||
return true;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
|
||||
std::size_t outputCount = 0;
|
||||
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
|
||||
FilterScanLine(input[lineIndex], output[lineIndex], params);
|
||||
outputCount += output[lineIndex].size();
|
||||
}
|
||||
FinishStats(stats, outputCount);
|
||||
return true;
|
||||
}
|
||||
if (params.method != PointCloudFilterMethod::RadiusOutlier
|
||||
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<FlatPoint> points;
|
||||
points.reserve(inputCount);
|
||||
for (const auto& line : input) {
|
||||
for (const SVzNL3DPosition& point : line) {
|
||||
if (IsFinite(point)) {
|
||||
points.push_back({point});
|
||||
}
|
||||
}
|
||||
}
|
||||
std::vector<SVzNL3DPosition> filtered;
|
||||
std::vector<SVzNL3DPosition> flatInput;
|
||||
flatInput.reserve(points.size());
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
flatInput.push_back(points[index].point);
|
||||
// Encode the flattened position only for reconstructing scan lines.
|
||||
flatInput.back().nPointIdx = static_cast<int>(index);
|
||||
}
|
||||
if (!Filter(flatInput, filtered, params, nullptr)) {
|
||||
output.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<bool> retained(points.size(), false);
|
||||
for (const SVzNL3DPosition& point : filtered) {
|
||||
if (point.nPointIdx >= 0
|
||||
&& static_cast<std::size_t>(point.nPointIdx) < retained.size()) {
|
||||
retained[static_cast<std::size_t>(point.nPointIdx)] = true;
|
||||
}
|
||||
}
|
||||
std::size_t pointIndex = 0;
|
||||
for (std::size_t lineIndex = 0; lineIndex < input.size(); ++lineIndex) {
|
||||
for (const SVzNL3DPosition& point : input[lineIndex]) {
|
||||
if (IsFinite(point)) {
|
||||
if (pointIndex < retained.size() && retained[pointIndex]) {
|
||||
output[lineIndex].push_back(point);
|
||||
}
|
||||
++pointIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::size_t outputCount = 0;
|
||||
for (const auto& line : output) {
|
||||
outputCount += line.size();
|
||||
}
|
||||
FinishStats(stats, outputCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PointCloudFilter::Filter(const std::vector<SVzNL3DPosition>& input,
|
||||
std::vector<SVzNL3DPosition>& output,
|
||||
const PointCloudFilterParams& params,
|
||||
PointCloudFilterStats* stats)
|
||||
{
|
||||
ResetStats(stats, input.size());
|
||||
output.clear();
|
||||
if (params.method == PointCloudFilterMethod::None) {
|
||||
output = input;
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::ScanLineContinuity) {
|
||||
FilterScanLine(input, output, params);
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
if (params.method != PointCloudFilterMethod::RadiusOutlier
|
||||
&& params.method != PointCloudFilterMethod::StatisticalOutlier) {
|
||||
return false;
|
||||
}
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier && params.radius <= 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<FlatPoint> points;
|
||||
points.reserve(input.size());
|
||||
for (const SVzNL3DPosition& point : input) {
|
||||
if (IsFinite(point)) {
|
||||
points.push_back({point});
|
||||
}
|
||||
}
|
||||
if (points.empty()) {
|
||||
FinishStats(stats, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
const double requestedRadius = params.method == PointCloudFilterMethod::RadiusOutlier
|
||||
? params.radius : params.searchRadius;
|
||||
const double radius = requestedRadius > 0.0 ? requestedRadius : EstimateCellSize(points) * 2.0;
|
||||
const double cellSize = std::max(radius, 1e-6);
|
||||
std::vector<bool> keep(points.size(), false);
|
||||
|
||||
if (params.method == PointCloudFilterMethod::RadiusOutlier) {
|
||||
const std::size_t minNeighbors = params.minNeighbors;
|
||||
FilterWithGrid(points, cellSize, radius,
|
||||
[&points, minNeighbors](std::size_t index, const std::vector<std::size_t>& candidates,
|
||||
double radiusSquared) {
|
||||
std::size_t neighbors = 0;
|
||||
for (std::size_t candidate : candidates) {
|
||||
if (candidate != index && SquaredDistance(points[index].point, points[candidate].point) <= radiusSquared) {
|
||||
++neighbors;
|
||||
}
|
||||
}
|
||||
return neighbors >= minNeighbors;
|
||||
}, keep);
|
||||
}
|
||||
|
||||
if (params.method == PointCloudFilterMethod::StatisticalOutlier) {
|
||||
std::vector<double> meanDistances(points.size(), std::numeric_limits<double>::infinity());
|
||||
FilterWithGrid(points, cellSize, radius,
|
||||
[&points, &meanDistances, ¶ms](std::size_t index,
|
||||
const std::vector<std::size_t>& candidates,
|
||||
double radiusSquared) {
|
||||
std::vector<double> distances;
|
||||
distances.reserve(candidates.size());
|
||||
for (std::size_t candidate : candidates) {
|
||||
if (candidate != index) {
|
||||
const double distance = SquaredDistance(points[index].point, points[candidate].point);
|
||||
if (distance <= radiusSquared) {
|
||||
distances.push_back(std::sqrt(distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (distances.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t count = std::min(params.meanK == 0 ? std::size_t(1) : params.meanK,
|
||||
distances.size());
|
||||
if (count < distances.size()) {
|
||||
std::nth_element(distances.begin(),
|
||||
distances.begin() + static_cast<std::ptrdiff_t>(count),
|
||||
distances.end());
|
||||
}
|
||||
meanDistances[index] = std::accumulate(distances.begin(), distances.begin() + static_cast<std::ptrdiff_t>(count), 0.0)
|
||||
/ static_cast<double>(count);
|
||||
return true;
|
||||
}, keep);
|
||||
std::vector<double> validMeans;
|
||||
for (double value : meanDistances) {
|
||||
if (std::isfinite(value)) {
|
||||
validMeans.push_back(value);
|
||||
}
|
||||
}
|
||||
if (!validMeans.empty()) {
|
||||
const double mean = std::accumulate(validMeans.begin(), validMeans.end(), 0.0)
|
||||
/ static_cast<double>(validMeans.size());
|
||||
double variance = 0.0;
|
||||
for (double value : validMeans) {
|
||||
const double delta = value - mean;
|
||||
variance += delta * delta;
|
||||
}
|
||||
const double deviation = std::sqrt(variance / static_cast<double>(validMeans.size()));
|
||||
const double threshold = mean + std::max(0.0, params.standardDeviationMultiplier) * deviation;
|
||||
for (std::size_t index = 0; index < keep.size(); ++index) {
|
||||
keep[index] = std::isfinite(meanDistances[index]) && meanDistances[index] <= threshold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.reserve(points.size());
|
||||
for (std::size_t index = 0; index < points.size(); ++index) {
|
||||
if (keep[index]) {
|
||||
output.push_back(points[index].point);
|
||||
}
|
||||
}
|
||||
FinishStats(stats, output.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1,30 +1,30 @@
|
||||
QT += core gui widgets opengl
|
||||
|
||||
CONFIG += c++17 staticlib
|
||||
TEMPLATE = lib
|
||||
TARGET = CloudView3D
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
QMAKE_CXXFLAGS += /bigobj
|
||||
}
|
||||
|
||||
INCLUDEPATH += $$PWD/Inc
|
||||
INCLUDEPATH += $$PWD/../VrUtils/Inc
|
||||
INCLUDEPATH += $$PWD/../VrCommon/Inc
|
||||
# VZNL_Types.h is maintained in the shared Utils module.
|
||||
|
||||
HEADERS += \
|
||||
Inc/CloudView3D.h \
|
||||
Inc/CloudView3DTypes.h \
|
||||
Inc/PointCloudGLWidget.h
|
||||
|
||||
SOURCES += \
|
||||
Src/PointCloudGLWidget.cpp
|
||||
|
||||
win32 {
|
||||
LIBS += -lopengl32
|
||||
LIBS += -lglu32
|
||||
}
|
||||
|
||||
DEFINES += _USE_MATH_DEFINES
|
||||
QT += core gui widgets opengl
|
||||
|
||||
CONFIG += c++17 staticlib
|
||||
TEMPLATE = lib
|
||||
TARGET = CloudView3D
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
QMAKE_CXXFLAGS += /bigobj
|
||||
}
|
||||
|
||||
INCLUDEPATH += $$PWD/Inc
|
||||
INCLUDEPATH += $$PWD/../VrUtils/Inc
|
||||
INCLUDEPATH += $$PWD/../VrCommon/Inc
|
||||
# VZNL_Types.h is maintained in the shared Utils module.
|
||||
|
||||
HEADERS += \
|
||||
Inc/CloudView3D.h \
|
||||
Inc/CloudView3DTypes.h \
|
||||
Inc/PointCloudGLWidget.h
|
||||
|
||||
SOURCES += \
|
||||
Src/PointCloudGLWidget.cpp
|
||||
|
||||
win32 {
|
||||
LIBS += -lopengl32
|
||||
LIBS += -lglu32
|
||||
}
|
||||
|
||||
DEFINES += _USE_MATH_DEFINES
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#ifndef CLOUD_VIEW_3D_H
|
||||
#define CLOUD_VIEW_3D_H
|
||||
|
||||
#include "CloudView3DTypes.h"
|
||||
#include "PointCloudGLWidget.h"
|
||||
|
||||
#endif // CLOUD_VIEW_3D_H
|
||||
#ifndef CLOUD_VIEW_3D_H
|
||||
#define CLOUD_VIEW_3D_H
|
||||
|
||||
#include "CloudView3DTypes.h"
|
||||
#include "PointCloudGLWidget.h"
|
||||
|
||||
#endif // CLOUD_VIEW_3D_H
|
||||
|
||||
@ -1,77 +1,77 @@
|
||||
#ifndef CLOUD_VIEW_3D_TYPES_H
|
||||
#define CLOUD_VIEW_3D_TYPES_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
struct Point3D
|
||||
{
|
||||
float x, y, z; // millimetres
|
||||
Point3D() : x(0), y(0), z(0) {}
|
||||
Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
|
||||
};
|
||||
|
||||
struct Point3DRGB
|
||||
{
|
||||
float x, y, z; // millimetres
|
||||
uint8_t r, g, b;
|
||||
float pointSize;
|
||||
|
||||
Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {}
|
||||
Point3DRGB(float _x,
|
||||
float _y,
|
||||
float _z,
|
||||
uint8_t _r = 255,
|
||||
uint8_t _g = 255,
|
||||
uint8_t _b = 255,
|
||||
float _ps = 0)
|
||||
: x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {}
|
||||
};
|
||||
|
||||
template<typename PointT>
|
||||
class SimplePointCloud
|
||||
{
|
||||
public:
|
||||
std::vector<PointT> points;
|
||||
std::vector<int> lineIndices;
|
||||
// Each point's original position within its scan line (-1 when unknown).
|
||||
std::vector<int> pointInLineIndices;
|
||||
|
||||
void clear()
|
||||
{
|
||||
points.clear();
|
||||
lineIndices.clear();
|
||||
pointInLineIndices.clear();
|
||||
}
|
||||
size_t size() const { return points.size(); }
|
||||
bool empty() const { return points.empty(); }
|
||||
void reserve(size_t n)
|
||||
{
|
||||
points.reserve(n);
|
||||
lineIndices.reserve(n);
|
||||
pointInLineIndices.reserve(n);
|
||||
}
|
||||
void push_back(const PointT& pt)
|
||||
{
|
||||
points.push_back(pt);
|
||||
pointInLineIndices.push_back(-1);
|
||||
}
|
||||
void push_back(const PointT& pt, int lineIdx)
|
||||
{
|
||||
points.push_back(pt);
|
||||
lineIndices.push_back(lineIdx);
|
||||
pointInLineIndices.push_back(-1);
|
||||
}
|
||||
void push_back(const PointT& pt, int lineIdx, int pointInLineIdx)
|
||||
{
|
||||
points.push_back(pt);
|
||||
lineIndices.push_back(lineIdx);
|
||||
pointInLineIndices.push_back(pointInLineIdx);
|
||||
}
|
||||
};
|
||||
|
||||
using PointCloudXYZ = SimplePointCloud<Point3D>;
|
||||
using PointCloudXYZRGB = SimplePointCloud<Point3DRGB>;
|
||||
|
||||
#endif // CLOUD_VIEW_3D_TYPES_H
|
||||
#ifndef CLOUD_VIEW_3D_TYPES_H
|
||||
#define CLOUD_VIEW_3D_TYPES_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
struct Point3D
|
||||
{
|
||||
float x, y, z; // millimetres
|
||||
Point3D() : x(0), y(0), z(0) {}
|
||||
Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
|
||||
};
|
||||
|
||||
struct Point3DRGB
|
||||
{
|
||||
float x, y, z; // millimetres
|
||||
uint8_t r, g, b;
|
||||
float pointSize;
|
||||
|
||||
Point3DRGB() : x(0), y(0), z(0), r(255), g(255), b(255), pointSize(0) {}
|
||||
Point3DRGB(float _x,
|
||||
float _y,
|
||||
float _z,
|
||||
uint8_t _r = 255,
|
||||
uint8_t _g = 255,
|
||||
uint8_t _b = 255,
|
||||
float _ps = 0)
|
||||
: x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_ps) {}
|
||||
};
|
||||
|
||||
template<typename PointT>
|
||||
class SimplePointCloud
|
||||
{
|
||||
public:
|
||||
std::vector<PointT> points;
|
||||
std::vector<int> lineIndices;
|
||||
// Each point's original position within its scan line (-1 when unknown).
|
||||
std::vector<int> pointInLineIndices;
|
||||
|
||||
void clear()
|
||||
{
|
||||
points.clear();
|
||||
lineIndices.clear();
|
||||
pointInLineIndices.clear();
|
||||
}
|
||||
size_t size() const { return points.size(); }
|
||||
bool empty() const { return points.empty(); }
|
||||
void reserve(size_t n)
|
||||
{
|
||||
points.reserve(n);
|
||||
lineIndices.reserve(n);
|
||||
pointInLineIndices.reserve(n);
|
||||
}
|
||||
void push_back(const PointT& pt)
|
||||
{
|
||||
points.push_back(pt);
|
||||
pointInLineIndices.push_back(-1);
|
||||
}
|
||||
void push_back(const PointT& pt, int lineIdx)
|
||||
{
|
||||
points.push_back(pt);
|
||||
lineIndices.push_back(lineIdx);
|
||||
pointInLineIndices.push_back(-1);
|
||||
}
|
||||
void push_back(const PointT& pt, int lineIdx, int pointInLineIdx)
|
||||
{
|
||||
points.push_back(pt);
|
||||
lineIndices.push_back(lineIdx);
|
||||
pointInLineIndices.push_back(pointInLineIdx);
|
||||
}
|
||||
};
|
||||
|
||||
using PointCloudXYZ = SimplePointCloud<Point3D>;
|
||||
using PointCloudXYZRGB = SimplePointCloud<Point3DRGB>;
|
||||
|
||||
#endif // CLOUD_VIEW_3D_TYPES_H
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
#ifndef CLOUD_VIEW_SETTINGS_H
|
||||
#define CLOUD_VIEW_SETTINGS_H
|
||||
|
||||
#include <QVector>
|
||||
#include <QVector3D>
|
||||
|
||||
#include "PointCloudXYZRange.h"
|
||||
|
||||
struct CloudViewSettingsData
|
||||
{
|
||||
float poseAxisLength = 10.0f;
|
||||
bool pseudoColorEnabled = true;
|
||||
float pseudoColorPeriod = 500.0f;
|
||||
QVector3D pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
|
||||
QVector3D pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
|
||||
bool pseudoColorDefaultPalette = true;
|
||||
PointCloudXYZRange pointCloudRange;
|
||||
};
|
||||
|
||||
class CloudViewSettings
|
||||
{
|
||||
public:
|
||||
static CloudViewSettingsData load();
|
||||
static bool save(const CloudViewSettingsData& settings);
|
||||
};
|
||||
|
||||
#endif // CLOUD_VIEW_SETTINGS_H
|
||||
#ifndef CLOUD_VIEW_SETTINGS_H
|
||||
#define CLOUD_VIEW_SETTINGS_H
|
||||
|
||||
#include <QVector>
|
||||
#include <QVector3D>
|
||||
|
||||
#include "PointCloudXYZRange.h"
|
||||
|
||||
struct CloudViewSettingsData
|
||||
{
|
||||
float poseAxisLength = 10.0f;
|
||||
bool pseudoColorEnabled = true;
|
||||
float pseudoColorPeriod = 500.0f;
|
||||
QVector3D pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
|
||||
QVector3D pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
|
||||
bool pseudoColorDefaultPalette = true;
|
||||
PointCloudXYZRange pointCloudRange;
|
||||
};
|
||||
|
||||
class CloudViewSettings
|
||||
{
|
||||
public:
|
||||
static CloudViewSettingsData load();
|
||||
static bool save(const CloudViewSettingsData& settings);
|
||||
};
|
||||
|
||||
#endif // CLOUD_VIEW_SETTINGS_H
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#ifndef CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
#define CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
|
||||
#include "../../CloudView3D/Inc/PointCloudGLWidget.h"
|
||||
|
||||
#endif // CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
#ifndef CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
#define CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
|
||||
#include "../../CloudView3D/Inc/PointCloudGLWidget.h"
|
||||
|
||||
#endif // CLOUD_VIEW_POINT_CLOUD_GL_WIDGET_FORWARDER_H
|
||||
|
||||
@ -1,171 +1,171 @@
|
||||
#include "CloudViewSettings.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString settingsFilePath()
|
||||
{
|
||||
QString settingsDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
if (settingsDir.isEmpty()) {
|
||||
settingsDir = QDir::homePath() + "/CloudView";
|
||||
}
|
||||
QDir().mkpath(settingsDir);
|
||||
return QDir(settingsDir).filePath("CloudView.ini");
|
||||
}
|
||||
}
|
||||
|
||||
CloudViewSettingsData CloudViewSettings::load()
|
||||
{
|
||||
CloudViewSettingsData result;
|
||||
QSettings settings(settingsFilePath(), QSettings::IniFormat);
|
||||
|
||||
// 模式独立于启用状态保存;缺失或非法配置默认采用“替换为全 0”。
|
||||
const QVariant savedModeValue = settings.contains("PointCloudRange/filterMode")
|
||||
? settings.value("PointCloudRange/filterMode")
|
||||
: settings.value("PointCloudRange/mode",
|
||||
static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero));
|
||||
const int savedFilterMode = savedModeValue.toInt();
|
||||
const PointCloudXYZRangeFilterMode filterMode =
|
||||
savedFilterMode == static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
|
||||
? PointCloudXYZRangeFilterMode::Remove
|
||||
: PointCloudXYZRangeFilterMode::ReplaceWithZero;
|
||||
|
||||
const float poseAxisLength = settings.value("Display/poseAxisLength", result.poseAxisLength).toFloat();
|
||||
if (std::isfinite(poseAxisLength) && poseAxisLength > 0.0f) {
|
||||
result.poseAxisLength = poseAxisLength;
|
||||
}
|
||||
|
||||
result.pseudoColorEnabled = settings.value("PseudoColor/enabled", result.pseudoColorEnabled).toBool();
|
||||
const bool hasPaletteSetting = settings.contains("PseudoColor/defaultPalette");
|
||||
result.pseudoColorDefaultPalette = hasPaletteSetting
|
||||
? settings.value("PseudoColor/defaultPalette").toBool()
|
||||
: result.pseudoColorDefaultPalette;
|
||||
const float period = settings.value("PseudoColor/period", result.pseudoColorPeriod).toFloat();
|
||||
if (std::isfinite(period) && period > 0.0f) {
|
||||
result.pseudoColorPeriod = period;
|
||||
}
|
||||
const QColor startColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
|
||||
const QColor endColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
|
||||
if (startColor.isValid()) {
|
||||
result.pseudoColorStartColor = QVector3D(startColor.redF(), startColor.greenF(), startColor.blueF());
|
||||
}
|
||||
if (endColor.isValid()) {
|
||||
result.pseudoColorEndColor = QVector3D(endColor.redF(), endColor.greenF(), endColor.blueF());
|
||||
}
|
||||
|
||||
if (!hasPaletteSetting && settings.contains("PseudoColor/period")) {
|
||||
const QColor legacyStartColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
|
||||
const QColor legacyEndColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
|
||||
const bool legacyDefaults = qFuzzyCompare(result.pseudoColorPeriod, 100.0f) &&
|
||||
legacyStartColor == QColor("#0000ff") &&
|
||||
legacyEndColor == QColor("#ff0000");
|
||||
if (legacyDefaults) {
|
||||
// 旧版周期色环配置迁移为 Z 范围蓝→红梯度。
|
||||
result.pseudoColorPeriod = 500.0f;
|
||||
result.pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
|
||||
result.pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
|
||||
result.pseudoColorDefaultPalette = true;
|
||||
} else {
|
||||
// 非默认旧配置继续使用原有的自定义起止色渐变。
|
||||
result.pseudoColorDefaultPalette = false;
|
||||
}
|
||||
}
|
||||
|
||||
const bool rangeEnabled = settings.value("PointCloudRange/enabled", false).toBool();
|
||||
PointCloudXYZRange range;
|
||||
range.enabled = rangeEnabled;
|
||||
range.filterMode = filterMode;
|
||||
if (rangeEnabled) {
|
||||
const auto readBound = [&settings](const char* key, double fallback) {
|
||||
bool ok = false;
|
||||
const double value = settings.value(key).toDouble(&ok);
|
||||
return ok && std::isfinite(value) ? value : fallback;
|
||||
};
|
||||
// Missing bounds retain the range object's implicit unbounded side.
|
||||
range.minX = readBound("PointCloudRange/minX", range.minX);
|
||||
range.maxX = readBound("PointCloudRange/maxX", range.maxX);
|
||||
range.minY = readBound("PointCloudRange/minY", range.minY);
|
||||
range.maxY = readBound("PointCloudRange/maxY", range.maxY);
|
||||
range.minZ = readBound("PointCloudRange/minZ", range.minZ);
|
||||
range.maxZ = readBound("PointCloudRange/maxZ", range.maxZ);
|
||||
if (range.isValid()) {
|
||||
result.pointCloudRange = range;
|
||||
}
|
||||
} else {
|
||||
result.pointCloudRange = range;
|
||||
}
|
||||
// Keep the mode even when a persisted enabled range is invalid and is
|
||||
// therefore reset to the disabled default range.
|
||||
result.pointCloudRange.filterMode = filterMode;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CloudViewSettings::save(const CloudViewSettingsData& value)
|
||||
{
|
||||
QSettings settings(settingsFilePath(), QSettings::IniFormat);
|
||||
if (std::isfinite(value.poseAxisLength) && value.poseAxisLength > 0.0f) {
|
||||
settings.setValue("Display/poseAxisLength", value.poseAxisLength);
|
||||
}
|
||||
|
||||
settings.setValue("PseudoColor/enabled", value.pseudoColorEnabled);
|
||||
settings.setValue("PseudoColor/defaultPalette", value.pseudoColorDefaultPalette);
|
||||
settings.setValue("PseudoColor/period", qMax(1.0f, value.pseudoColorPeriod));
|
||||
settings.setValue("PseudoColor/startColor",
|
||||
QColor::fromRgbF(value.pseudoColorStartColor.x(),
|
||||
value.pseudoColorStartColor.y(),
|
||||
value.pseudoColorStartColor.z()).name(QColor::HexRgb));
|
||||
settings.setValue("PseudoColor/endColor",
|
||||
QColor::fromRgbF(value.pseudoColorEndColor.x(),
|
||||
value.pseudoColorEndColor.y(),
|
||||
value.pseudoColorEndColor.z()).name(QColor::HexRgb));
|
||||
|
||||
const PointCloudXYZRange& range = value.pointCloudRange;
|
||||
const bool saveRange = range.enabled && range.isValid();
|
||||
settings.setValue("PointCloudRange/enabled", saveRange);
|
||||
const int filterModeValue = range.filterMode == PointCloudXYZRangeFilterMode::Remove
|
||||
? static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
|
||||
: static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero);
|
||||
settings.setValue("PointCloudRange/filterMode", filterModeValue);
|
||||
settings.setValue("PointCloudRange/mode", filterModeValue);
|
||||
if (saveRange) {
|
||||
const auto saveBound = [&settings](const char* key, double bound) {
|
||||
if (std::isfinite(bound)) {
|
||||
settings.setValue(key, bound);
|
||||
} else {
|
||||
settings.remove(key);
|
||||
}
|
||||
};
|
||||
saveBound("PointCloudRange/minX", range.minX);
|
||||
saveBound("PointCloudRange/maxX", range.maxX);
|
||||
saveBound("PointCloudRange/minY", range.minY);
|
||||
saveBound("PointCloudRange/maxY", range.maxY);
|
||||
saveBound("PointCloudRange/minZ", range.minZ);
|
||||
saveBound("PointCloudRange/maxZ", range.maxZ);
|
||||
} else {
|
||||
// Disabled ranges use implicit infinite bounds; do not persist infinities.
|
||||
settings.remove("PointCloudRange/minX");
|
||||
settings.remove("PointCloudRange/maxX");
|
||||
settings.remove("PointCloudRange/minY");
|
||||
settings.remove("PointCloudRange/maxY");
|
||||
settings.remove("PointCloudRange/minZ");
|
||||
settings.remove("PointCloudRange/maxZ");
|
||||
}
|
||||
|
||||
// Remove keys from the former min/max/stop based implementation so old
|
||||
// values cannot reappear in the user configuration file.
|
||||
settings.remove("PseudoColor/autoRange");
|
||||
settings.remove("PseudoColor/minZ");
|
||||
settings.remove("PseudoColor/maxZ");
|
||||
settings.remove("PseudoColor/divisionCount");
|
||||
settings.remove("PseudoColor/gradient");
|
||||
settings.remove("PseudoColor/stops");
|
||||
|
||||
settings.sync();
|
||||
return settings.status() == QSettings::NoError;
|
||||
}
|
||||
#include "CloudViewSettings.h"
|
||||
|
||||
#include <QColor>
|
||||
#include <QDir>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
QString settingsFilePath()
|
||||
{
|
||||
QString settingsDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
if (settingsDir.isEmpty()) {
|
||||
settingsDir = QDir::homePath() + "/CloudView";
|
||||
}
|
||||
QDir().mkpath(settingsDir);
|
||||
return QDir(settingsDir).filePath("CloudView.ini");
|
||||
}
|
||||
}
|
||||
|
||||
CloudViewSettingsData CloudViewSettings::load()
|
||||
{
|
||||
CloudViewSettingsData result;
|
||||
QSettings settings(settingsFilePath(), QSettings::IniFormat);
|
||||
|
||||
// 模式独立于启用状态保存;缺失或非法配置默认采用“替换为全 0”。
|
||||
const QVariant savedModeValue = settings.contains("PointCloudRange/filterMode")
|
||||
? settings.value("PointCloudRange/filterMode")
|
||||
: settings.value("PointCloudRange/mode",
|
||||
static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero));
|
||||
const int savedFilterMode = savedModeValue.toInt();
|
||||
const PointCloudXYZRangeFilterMode filterMode =
|
||||
savedFilterMode == static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
|
||||
? PointCloudXYZRangeFilterMode::Remove
|
||||
: PointCloudXYZRangeFilterMode::ReplaceWithZero;
|
||||
|
||||
const float poseAxisLength = settings.value("Display/poseAxisLength", result.poseAxisLength).toFloat();
|
||||
if (std::isfinite(poseAxisLength) && poseAxisLength > 0.0f) {
|
||||
result.poseAxisLength = poseAxisLength;
|
||||
}
|
||||
|
||||
result.pseudoColorEnabled = settings.value("PseudoColor/enabled", result.pseudoColorEnabled).toBool();
|
||||
const bool hasPaletteSetting = settings.contains("PseudoColor/defaultPalette");
|
||||
result.pseudoColorDefaultPalette = hasPaletteSetting
|
||||
? settings.value("PseudoColor/defaultPalette").toBool()
|
||||
: result.pseudoColorDefaultPalette;
|
||||
const float period = settings.value("PseudoColor/period", result.pseudoColorPeriod).toFloat();
|
||||
if (std::isfinite(period) && period > 0.0f) {
|
||||
result.pseudoColorPeriod = period;
|
||||
}
|
||||
const QColor startColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
|
||||
const QColor endColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
|
||||
if (startColor.isValid()) {
|
||||
result.pseudoColorStartColor = QVector3D(startColor.redF(), startColor.greenF(), startColor.blueF());
|
||||
}
|
||||
if (endColor.isValid()) {
|
||||
result.pseudoColorEndColor = QVector3D(endColor.redF(), endColor.greenF(), endColor.blueF());
|
||||
}
|
||||
|
||||
if (!hasPaletteSetting && settings.contains("PseudoColor/period")) {
|
||||
const QColor legacyStartColor(settings.value("PseudoColor/startColor", "#0000ff").toString());
|
||||
const QColor legacyEndColor(settings.value("PseudoColor/endColor", "#ff0000").toString());
|
||||
const bool legacyDefaults = qFuzzyCompare(result.pseudoColorPeriod, 100.0f) &&
|
||||
legacyStartColor == QColor("#0000ff") &&
|
||||
legacyEndColor == QColor("#ff0000");
|
||||
if (legacyDefaults) {
|
||||
// 旧版周期色环配置迁移为 Z 范围蓝→红梯度。
|
||||
result.pseudoColorPeriod = 500.0f;
|
||||
result.pseudoColorStartColor = QVector3D(0.0f, 0.0f, 1.0f);
|
||||
result.pseudoColorEndColor = QVector3D(1.0f, 0.0f, 0.0f);
|
||||
result.pseudoColorDefaultPalette = true;
|
||||
} else {
|
||||
// 非默认旧配置继续使用原有的自定义起止色渐变。
|
||||
result.pseudoColorDefaultPalette = false;
|
||||
}
|
||||
}
|
||||
|
||||
const bool rangeEnabled = settings.value("PointCloudRange/enabled", false).toBool();
|
||||
PointCloudXYZRange range;
|
||||
range.enabled = rangeEnabled;
|
||||
range.filterMode = filterMode;
|
||||
if (rangeEnabled) {
|
||||
const auto readBound = [&settings](const char* key, double fallback) {
|
||||
bool ok = false;
|
||||
const double value = settings.value(key).toDouble(&ok);
|
||||
return ok && std::isfinite(value) ? value : fallback;
|
||||
};
|
||||
// Missing bounds retain the range object's implicit unbounded side.
|
||||
range.minX = readBound("PointCloudRange/minX", range.minX);
|
||||
range.maxX = readBound("PointCloudRange/maxX", range.maxX);
|
||||
range.minY = readBound("PointCloudRange/minY", range.minY);
|
||||
range.maxY = readBound("PointCloudRange/maxY", range.maxY);
|
||||
range.minZ = readBound("PointCloudRange/minZ", range.minZ);
|
||||
range.maxZ = readBound("PointCloudRange/maxZ", range.maxZ);
|
||||
if (range.isValid()) {
|
||||
result.pointCloudRange = range;
|
||||
}
|
||||
} else {
|
||||
result.pointCloudRange = range;
|
||||
}
|
||||
// Keep the mode even when a persisted enabled range is invalid and is
|
||||
// therefore reset to the disabled default range.
|
||||
result.pointCloudRange.filterMode = filterMode;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CloudViewSettings::save(const CloudViewSettingsData& value)
|
||||
{
|
||||
QSettings settings(settingsFilePath(), QSettings::IniFormat);
|
||||
if (std::isfinite(value.poseAxisLength) && value.poseAxisLength > 0.0f) {
|
||||
settings.setValue("Display/poseAxisLength", value.poseAxisLength);
|
||||
}
|
||||
|
||||
settings.setValue("PseudoColor/enabled", value.pseudoColorEnabled);
|
||||
settings.setValue("PseudoColor/defaultPalette", value.pseudoColorDefaultPalette);
|
||||
settings.setValue("PseudoColor/period", qMax(1.0f, value.pseudoColorPeriod));
|
||||
settings.setValue("PseudoColor/startColor",
|
||||
QColor::fromRgbF(value.pseudoColorStartColor.x(),
|
||||
value.pseudoColorStartColor.y(),
|
||||
value.pseudoColorStartColor.z()).name(QColor::HexRgb));
|
||||
settings.setValue("PseudoColor/endColor",
|
||||
QColor::fromRgbF(value.pseudoColorEndColor.x(),
|
||||
value.pseudoColorEndColor.y(),
|
||||
value.pseudoColorEndColor.z()).name(QColor::HexRgb));
|
||||
|
||||
const PointCloudXYZRange& range = value.pointCloudRange;
|
||||
const bool saveRange = range.enabled && range.isValid();
|
||||
settings.setValue("PointCloudRange/enabled", saveRange);
|
||||
const int filterModeValue = range.filterMode == PointCloudXYZRangeFilterMode::Remove
|
||||
? static_cast<int>(PointCloudXYZRangeFilterMode::Remove)
|
||||
: static_cast<int>(PointCloudXYZRangeFilterMode::ReplaceWithZero);
|
||||
settings.setValue("PointCloudRange/filterMode", filterModeValue);
|
||||
settings.setValue("PointCloudRange/mode", filterModeValue);
|
||||
if (saveRange) {
|
||||
const auto saveBound = [&settings](const char* key, double bound) {
|
||||
if (std::isfinite(bound)) {
|
||||
settings.setValue(key, bound);
|
||||
} else {
|
||||
settings.remove(key);
|
||||
}
|
||||
};
|
||||
saveBound("PointCloudRange/minX", range.minX);
|
||||
saveBound("PointCloudRange/maxX", range.maxX);
|
||||
saveBound("PointCloudRange/minY", range.minY);
|
||||
saveBound("PointCloudRange/maxY", range.maxY);
|
||||
saveBound("PointCloudRange/minZ", range.minZ);
|
||||
saveBound("PointCloudRange/maxZ", range.maxZ);
|
||||
} else {
|
||||
// Disabled ranges use implicit infinite bounds; do not persist infinities.
|
||||
settings.remove("PointCloudRange/minX");
|
||||
settings.remove("PointCloudRange/maxX");
|
||||
settings.remove("PointCloudRange/minY");
|
||||
settings.remove("PointCloudRange/maxY");
|
||||
settings.remove("PointCloudRange/minZ");
|
||||
settings.remove("PointCloudRange/maxZ");
|
||||
}
|
||||
|
||||
// Remove keys from the former min/max/stop based implementation so old
|
||||
// values cannot reappear in the user configuration file.
|
||||
settings.remove("PseudoColor/autoRange");
|
||||
settings.remove("PseudoColor/minZ");
|
||||
settings.remove("PseudoColor/maxZ");
|
||||
settings.remove("PseudoColor/divisionCount");
|
||||
settings.remove("PseudoColor/gradient");
|
||||
settings.remove("PseudoColor/stops");
|
||||
|
||||
settings.sync();
|
||||
return settings.status() == QSettings::NoError;
|
||||
}
|
||||
|
||||
@ -1,102 +1,102 @@
|
||||
#include <QApplication>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QIcon>
|
||||
#include <QCommandLineParser>
|
||||
#include <QTimer>
|
||||
#include "CloudViewMainWindow.h"
|
||||
|
||||
#define APP_VERSION "1.2.1"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置 OpenGL 格式 - 使用 2.1 版本以确保固定管线兼容
|
||||
QSurfaceFormat format;
|
||||
format.setDepthBufferSize(24);
|
||||
format.setStencilBufferSize(8);
|
||||
format.setVersion(2, 1); // 使用 OpenGL 2.1 以确保固定管线函数可用
|
||||
format.setSamples(4); // 抗锯齿
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
|
||||
// 设置应用信息
|
||||
app.setApplicationName("CloudViewApp");
|
||||
app.setOrganizationName("CloudViewApp");
|
||||
app.setApplicationVersion(APP_VERSION);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("CloudView point cloud and shape viewer");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
QCommandLineOption cloudOption(
|
||||
QStringList() << "cloud" << "point-cloud",
|
||||
"Load a point cloud or segment file. Positional files are treated the same way.",
|
||||
"file");
|
||||
QCommandLineOption bboxOption(
|
||||
QStringList() << "bbox",
|
||||
"Load a bounding-box JSON file.",
|
||||
"file");
|
||||
QCommandLineOption circleOption(
|
||||
QStringList() << "circle",
|
||||
"Load a circle data file. Row format: {x,y,z}-{r,p,y}-{radius}.",
|
||||
"file");
|
||||
QCommandLineOption rectangleOption(
|
||||
QStringList() << "rectangle",
|
||||
"Load a rectangle data file. Row format: {x,y,z}-{r,p,y}-{width,height}.",
|
||||
"file");
|
||||
QCommandLineOption surfaceOption(
|
||||
QStringList() << "surface",
|
||||
"Load a surface data file. Row format: {x,y,z}-{length,width} or {x,y,z}-{r,p,y}-{length,width}.",
|
||||
"file");
|
||||
QCommandLineOption triangleOption(
|
||||
QStringList() << "triangle",
|
||||
"Load a triangle data file. Row format: {x,y,z}-{x,y,z}-{x,y,z}.",
|
||||
"file");
|
||||
QCommandLineOption sequenceOption(
|
||||
QStringList() << "sequence" << "sequential",
|
||||
"Display startup files one at a time. Closing the window loads the next file.");
|
||||
|
||||
parser.addOption(cloudOption);
|
||||
parser.addOption(bboxOption);
|
||||
parser.addOption(circleOption);
|
||||
parser.addOption(rectangleOption);
|
||||
parser.addOption(surfaceOption);
|
||||
parser.addOption(triangleOption);
|
||||
parser.addOption(sequenceOption);
|
||||
parser.addPositionalArgument("files", "Point cloud or segment files to load.");
|
||||
if (app.arguments().contains(QStringLiteral("--help")) ||
|
||||
app.arguments().contains(QStringLiteral("-h"))) {
|
||||
parser.showHelp(0);
|
||||
}
|
||||
if (app.arguments().contains(QStringLiteral("--version")) ||
|
||||
app.arguments().contains(QStringLiteral("-v"))) {
|
||||
parser.showVersion();
|
||||
}
|
||||
parser.process(app);
|
||||
|
||||
CloudViewMainWindow::StartupFiles startupFiles;
|
||||
startupFiles.displayFiles = parser.values(cloudOption);
|
||||
startupFiles.displayFiles.append(parser.positionalArguments());
|
||||
startupFiles.bboxFiles = parser.values(bboxOption);
|
||||
startupFiles.circleFiles = parser.values(circleOption);
|
||||
startupFiles.rectangleFiles = parser.values(rectangleOption);
|
||||
startupFiles.surfaceFiles = parser.values(surfaceOption);
|
||||
startupFiles.triangleFiles = parser.values(triangleOption);
|
||||
startupFiles.sequential = parser.isSet(sequenceOption);
|
||||
|
||||
// 设置应用程序图标
|
||||
app.setWindowIcon(QIcon(":/logo.png"));
|
||||
|
||||
// 创建并显示主窗口
|
||||
CloudViewMainWindow mainWindow;
|
||||
mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION));
|
||||
mainWindow.resize(1280, 720);
|
||||
mainWindow.show();
|
||||
|
||||
QTimer::singleShot(0, &mainWindow, [&mainWindow, startupFiles]() {
|
||||
mainWindow.loadStartupFiles(startupFiles);
|
||||
});
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
#include <QApplication>
|
||||
#include <QSurfaceFormat>
|
||||
#include <QIcon>
|
||||
#include <QCommandLineParser>
|
||||
#include <QTimer>
|
||||
#include "CloudViewMainWindow.h"
|
||||
|
||||
#define APP_VERSION "1.2.1"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
// 设置 OpenGL 格式 - 使用 2.1 版本以确保固定管线兼容
|
||||
QSurfaceFormat format;
|
||||
format.setDepthBufferSize(24);
|
||||
format.setStencilBufferSize(8);
|
||||
format.setVersion(2, 1); // 使用 OpenGL 2.1 以确保固定管线函数可用
|
||||
format.setSamples(4); // 抗锯齿
|
||||
QSurfaceFormat::setDefaultFormat(format);
|
||||
|
||||
// 设置应用信息
|
||||
app.setApplicationName("CloudViewApp");
|
||||
app.setOrganizationName("CloudViewApp");
|
||||
app.setApplicationVersion(APP_VERSION);
|
||||
|
||||
QCommandLineParser parser;
|
||||
parser.setApplicationDescription("CloudView point cloud and shape viewer");
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
QCommandLineOption cloudOption(
|
||||
QStringList() << "cloud" << "point-cloud",
|
||||
"Load a point cloud or segment file. Positional files are treated the same way.",
|
||||
"file");
|
||||
QCommandLineOption bboxOption(
|
||||
QStringList() << "bbox",
|
||||
"Load a bounding-box JSON file.",
|
||||
"file");
|
||||
QCommandLineOption circleOption(
|
||||
QStringList() << "circle",
|
||||
"Load a circle data file. Row format: {x,y,z}-{r,p,y}-{radius}.",
|
||||
"file");
|
||||
QCommandLineOption rectangleOption(
|
||||
QStringList() << "rectangle",
|
||||
"Load a rectangle data file. Row format: {x,y,z}-{r,p,y}-{width,height}.",
|
||||
"file");
|
||||
QCommandLineOption surfaceOption(
|
||||
QStringList() << "surface",
|
||||
"Load a surface data file. Row format: {x,y,z}-{length,width} or {x,y,z}-{r,p,y}-{length,width}.",
|
||||
"file");
|
||||
QCommandLineOption triangleOption(
|
||||
QStringList() << "triangle",
|
||||
"Load a triangle data file. Row format: {x,y,z}-{x,y,z}-{x,y,z}.",
|
||||
"file");
|
||||
QCommandLineOption sequenceOption(
|
||||
QStringList() << "sequence" << "sequential",
|
||||
"Display startup files one at a time. Closing the window loads the next file.");
|
||||
|
||||
parser.addOption(cloudOption);
|
||||
parser.addOption(bboxOption);
|
||||
parser.addOption(circleOption);
|
||||
parser.addOption(rectangleOption);
|
||||
parser.addOption(surfaceOption);
|
||||
parser.addOption(triangleOption);
|
||||
parser.addOption(sequenceOption);
|
||||
parser.addPositionalArgument("files", "Point cloud or segment files to load.");
|
||||
if (app.arguments().contains(QStringLiteral("--help")) ||
|
||||
app.arguments().contains(QStringLiteral("-h"))) {
|
||||
parser.showHelp(0);
|
||||
}
|
||||
if (app.arguments().contains(QStringLiteral("--version")) ||
|
||||
app.arguments().contains(QStringLiteral("-v"))) {
|
||||
parser.showVersion();
|
||||
}
|
||||
parser.process(app);
|
||||
|
||||
CloudViewMainWindow::StartupFiles startupFiles;
|
||||
startupFiles.displayFiles = parser.values(cloudOption);
|
||||
startupFiles.displayFiles.append(parser.positionalArguments());
|
||||
startupFiles.bboxFiles = parser.values(bboxOption);
|
||||
startupFiles.circleFiles = parser.values(circleOption);
|
||||
startupFiles.rectangleFiles = parser.values(rectangleOption);
|
||||
startupFiles.surfaceFiles = parser.values(surfaceOption);
|
||||
startupFiles.triangleFiles = parser.values(triangleOption);
|
||||
startupFiles.sequential = parser.isSet(sequenceOption);
|
||||
|
||||
// 设置应用程序图标
|
||||
app.setWindowIcon(QIcon(":/logo.png"));
|
||||
|
||||
// 创建并显示主窗口
|
||||
CloudViewMainWindow mainWindow;
|
||||
mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION));
|
||||
mainWindow.resize(1280, 720);
|
||||
mainWindow.show();
|
||||
|
||||
QTimer::singleShot(0, &mainWindow, [&mainWindow, startupFiles]() {
|
||||
mainWindow.loadStartupFiles(startupFiles);
|
||||
});
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
QT += core gui widgets
|
||||
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib c++11
|
||||
TARGET = ImageView
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
}
|
||||
|
||||
INCLUDEPATH += $$PWD/Inc
|
||||
|
||||
HEADERS += \
|
||||
Inc/ImageView.h
|
||||
|
||||
SOURCES += \
|
||||
Src/ImageView.cpp
|
||||
QT += core gui widgets
|
||||
|
||||
TEMPLATE = lib
|
||||
CONFIG += staticlib c++11
|
||||
TARGET = ImageView
|
||||
|
||||
win32-msvc {
|
||||
QMAKE_CXXFLAGS += /utf-8
|
||||
}
|
||||
|
||||
INCLUDEPATH += $$PWD/Inc
|
||||
|
||||
HEADERS += \
|
||||
Inc/ImageView.h
|
||||
|
||||
SOURCES += \
|
||||
Src/ImageView.cpp
|
||||
|
||||
@ -1,74 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <Qt>
|
||||
|
||||
class QLabel;
|
||||
class QGraphicsPixmapItem;
|
||||
class QGraphicsScene;
|
||||
class QMouseEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
// 可缩放、可平移的图片显示控件。
|
||||
class ImageView : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ImageView(QWidget* parent = nullptr);
|
||||
~ImageView() override;
|
||||
|
||||
void setImage(const QImage& image);
|
||||
void setImage(const QPixmap& pixmap);
|
||||
bool loadImage(const QString& filePath);
|
||||
void clearImage();
|
||||
|
||||
bool hasImage() const;
|
||||
QImage image() const;
|
||||
|
||||
// 比例以原始图片大小为基准,1.0 表示 100%。
|
||||
double zoomRatio() const;
|
||||
void setZoomRatio(double ratio);
|
||||
void zoomIn(double factor = 1.25);
|
||||
void zoomOut(double factor = 1.25);
|
||||
void resetView();
|
||||
|
||||
void setZoomRange(double minimum, double maximum);
|
||||
double minimumZoomRatio() const;
|
||||
double maximumZoomRatio() const;
|
||||
|
||||
// 图片未铺满视口时的停靠方向,默认居中。
|
||||
void setImageAlignment(Qt::Alignment alignment);
|
||||
Qt::Alignment imageAlignment() const;
|
||||
|
||||
signals:
|
||||
void imageChanged();
|
||||
void zoomRatioChanged(double ratio);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
void updateZoomLabel();
|
||||
void updateZoomRatio(double ratio, bool keepCenter);
|
||||
void repositionZoomLabel();
|
||||
|
||||
QGraphicsScene* m_scene;
|
||||
QGraphicsPixmapItem* m_pixmapItem;
|
||||
QLabel* m_zoomLabel;
|
||||
QImage m_image;
|
||||
double m_zoomRatio;
|
||||
double m_minimumZoomRatio;
|
||||
double m_maximumZoomRatio;
|
||||
Qt::Alignment m_imageAlignment;
|
||||
bool m_panning;
|
||||
bool m_needsInitialFit;
|
||||
QPoint m_lastMousePosition;
|
||||
};
|
||||
#pragma once
|
||||
|
||||
#include <QGraphicsView>
|
||||
#include <QImage>
|
||||
#include <QPixmap>
|
||||
#include <Qt>
|
||||
|
||||
class QLabel;
|
||||
class QGraphicsPixmapItem;
|
||||
class QGraphicsScene;
|
||||
class QMouseEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
// 可缩放、可平移的图片显示控件。
|
||||
class ImageView : public QGraphicsView
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ImageView(QWidget* parent = nullptr);
|
||||
~ImageView() override;
|
||||
|
||||
void setImage(const QImage& image);
|
||||
void setImage(const QPixmap& pixmap);
|
||||
bool loadImage(const QString& filePath);
|
||||
void clearImage();
|
||||
|
||||
bool hasImage() const;
|
||||
QImage image() const;
|
||||
|
||||
// 比例以原始图片大小为基准,1.0 表示 100%。
|
||||
double zoomRatio() const;
|
||||
void setZoomRatio(double ratio);
|
||||
void zoomIn(double factor = 1.25);
|
||||
void zoomOut(double factor = 1.25);
|
||||
void resetView();
|
||||
|
||||
void setZoomRange(double minimum, double maximum);
|
||||
double minimumZoomRatio() const;
|
||||
double maximumZoomRatio() const;
|
||||
|
||||
// 图片未铺满视口时的停靠方向,默认居中。
|
||||
void setImageAlignment(Qt::Alignment alignment);
|
||||
Qt::Alignment imageAlignment() const;
|
||||
|
||||
signals:
|
||||
void imageChanged();
|
||||
void zoomRatioChanged(double ratio);
|
||||
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
void wheelEvent(QWheelEvent* event) override;
|
||||
void mousePressEvent(QMouseEvent* event) override;
|
||||
void mouseMoveEvent(QMouseEvent* event) override;
|
||||
void mouseReleaseEvent(QMouseEvent* event) override;
|
||||
void mouseDoubleClickEvent(QMouseEvent* event) override;
|
||||
|
||||
private:
|
||||
void updateZoomLabel();
|
||||
void updateZoomRatio(double ratio, bool keepCenter);
|
||||
void repositionZoomLabel();
|
||||
|
||||
QGraphicsScene* m_scene;
|
||||
QGraphicsPixmapItem* m_pixmapItem;
|
||||
QLabel* m_zoomLabel;
|
||||
QImage m_image;
|
||||
double m_zoomRatio;
|
||||
double m_minimumZoomRatio;
|
||||
double m_maximumZoomRatio;
|
||||
Qt::Alignment m_imageAlignment;
|
||||
bool m_panning;
|
||||
bool m_needsInitialFit;
|
||||
QPoint m_lastMousePosition;
|
||||
};
|
||||
|
||||
@ -1,315 +1,315 @@
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QStyle>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
|
||||
namespace {
|
||||
constexpr double kDefaultMinimumZoom = 0.02;
|
||||
constexpr double kDefaultMaximumZoom = 20.0;
|
||||
constexpr double kZoomStep = 1.25;
|
||||
}
|
||||
|
||||
ImageView::ImageView(QWidget* parent)
|
||||
: QGraphicsView(parent),
|
||||
m_scene(new QGraphicsScene(this)),
|
||||
m_pixmapItem(nullptr),
|
||||
m_zoomLabel(new QLabel(this)),
|
||||
m_zoomRatio(1.0),
|
||||
m_minimumZoomRatio(kDefaultMinimumZoom),
|
||||
m_maximumZoomRatio(kDefaultMaximumZoom),
|
||||
m_imageAlignment(Qt::AlignCenter),
|
||||
m_panning(false),
|
||||
m_needsInitialFit(false)
|
||||
{
|
||||
setScene(m_scene);
|
||||
setBackgroundBrush(QColor(35, 35, 35));
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
|
||||
setResizeAnchor(QGraphicsView::AnchorViewCenter);
|
||||
setAlignment(m_imageAlignment);
|
||||
setDragMode(QGraphicsView::NoDrag);
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
|
||||
m_zoomLabel->setObjectName(QStringLiteral("imageViewZoomLabel"));
|
||||
m_zoomLabel->setAlignment(Qt::AlignCenter);
|
||||
m_zoomLabel->setMinimumWidth(58);
|
||||
m_zoomLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
m_zoomLabel->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: white; background: transparent; border: none; }"));
|
||||
updateZoomLabel();
|
||||
}
|
||||
|
||||
ImageView::~ImageView() = default;
|
||||
|
||||
void ImageView::setImage(const QImage& image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = image;
|
||||
if (!m_pixmapItem) {
|
||||
m_pixmapItem = m_scene->addPixmap(QPixmap::fromImage(m_image));
|
||||
} else {
|
||||
m_pixmapItem->setPixmap(QPixmap::fromImage(m_image));
|
||||
}
|
||||
m_scene->setSceneRect(QRectF(QPointF(0.0, 0.0), QSizeF(m_image.size())));
|
||||
m_needsInitialFit = true;
|
||||
if (viewport()->width() > 0 && viewport()->height() > 0) {
|
||||
m_needsInitialFit = false;
|
||||
resetView();
|
||||
}
|
||||
emit imageChanged();
|
||||
}
|
||||
|
||||
void ImageView::setImage(const QPixmap& pixmap)
|
||||
{
|
||||
setImage(pixmap.toImage());
|
||||
}
|
||||
|
||||
bool ImageView::loadImage(const QString& filePath)
|
||||
{
|
||||
QImage loaded;
|
||||
if (!loaded.load(filePath)) {
|
||||
return false;
|
||||
}
|
||||
setImage(loaded);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageView::clearImage()
|
||||
{
|
||||
m_image = QImage();
|
||||
m_scene->clear();
|
||||
m_pixmapItem = nullptr;
|
||||
m_scene->setSceneRect(QRectF());
|
||||
m_zoomRatio = 1.0;
|
||||
m_needsInitialFit = false;
|
||||
updateZoomLabel();
|
||||
viewport()->update();
|
||||
emit imageChanged();
|
||||
}
|
||||
|
||||
bool ImageView::hasImage() const
|
||||
{
|
||||
return !m_image.isNull();
|
||||
}
|
||||
|
||||
QImage ImageView::image() const
|
||||
{
|
||||
return m_image;
|
||||
}
|
||||
|
||||
double ImageView::zoomRatio() const
|
||||
{
|
||||
return m_zoomRatio;
|
||||
}
|
||||
|
||||
void ImageView::setZoomRatio(double ratio)
|
||||
{
|
||||
updateZoomRatio(ratio, true);
|
||||
}
|
||||
|
||||
void ImageView::zoomIn(double factor)
|
||||
{
|
||||
if (factor <= 1.0) {
|
||||
factor = kZoomStep;
|
||||
}
|
||||
updateZoomRatio(m_zoomRatio * factor, true);
|
||||
}
|
||||
|
||||
void ImageView::zoomOut(double factor)
|
||||
{
|
||||
if (factor <= 1.0) {
|
||||
factor = kZoomStep;
|
||||
}
|
||||
updateZoomRatio(m_zoomRatio / factor, true);
|
||||
}
|
||||
|
||||
void ImageView::resetView()
|
||||
{
|
||||
if (!hasImage() || !m_pixmapItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double previousRatio = m_zoomRatio;
|
||||
resetTransform();
|
||||
fitInView(m_pixmapItem, Qt::KeepAspectRatio);
|
||||
|
||||
const double fittedRatio = qBound(m_minimumZoomRatio,
|
||||
transform().m11(),
|
||||
m_maximumZoomRatio);
|
||||
resetTransform();
|
||||
scale(fittedRatio, fittedRatio);
|
||||
m_zoomRatio = fittedRatio;
|
||||
centerOn(m_pixmapItem);
|
||||
updateZoomLabel();
|
||||
|
||||
if (!qFuzzyCompare(previousRatio, m_zoomRatio)) {
|
||||
emit zoomRatioChanged(m_zoomRatio);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageView::setZoomRange(double minimum, double maximum)
|
||||
{
|
||||
if (minimum <= 0.0 || maximum < minimum) {
|
||||
return;
|
||||
}
|
||||
m_minimumZoomRatio = minimum;
|
||||
m_maximumZoomRatio = maximum;
|
||||
updateZoomRatio(m_zoomRatio, true);
|
||||
}
|
||||
|
||||
double ImageView::minimumZoomRatio() const
|
||||
{
|
||||
return m_minimumZoomRatio;
|
||||
}
|
||||
|
||||
double ImageView::maximumZoomRatio() const
|
||||
{
|
||||
return m_maximumZoomRatio;
|
||||
}
|
||||
|
||||
void ImageView::setImageAlignment(Qt::Alignment alignment)
|
||||
{
|
||||
if (!alignment) {
|
||||
alignment = Qt::AlignCenter;
|
||||
}
|
||||
m_imageAlignment = alignment;
|
||||
setAlignment(m_imageAlignment);
|
||||
if (hasImage()) {
|
||||
viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
Qt::Alignment ImageView::imageAlignment() const
|
||||
{
|
||||
return m_imageAlignment;
|
||||
}
|
||||
|
||||
void ImageView::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QGraphicsView::resizeEvent(event);
|
||||
if (m_needsInitialFit && hasImage() && viewport()->width() > 0 && viewport()->height() > 0) {
|
||||
m_needsInitialFit = false;
|
||||
resetView();
|
||||
}
|
||||
repositionZoomLabel();
|
||||
}
|
||||
|
||||
void ImageView::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (!hasImage() || event->angleDelta().y() == 0) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
const double factor = event->angleDelta().y() > 0 ? kZoomStep : 1.0 / kZoomStep;
|
||||
updateZoomRatio(m_zoomRatio * factor, true);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ImageView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && hasImage()) {
|
||||
m_panning = true;
|
||||
m_lastMousePosition = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
if (m_panning) {
|
||||
const QPoint delta = event->pos() - m_lastMousePosition;
|
||||
m_lastMousePosition = event->pos();
|
||||
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x());
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_panning) {
|
||||
m_panning = false;
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && hasImage()) {
|
||||
resetView();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseDoubleClickEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::updateZoomLabel()
|
||||
{
|
||||
if (!hasImage()) {
|
||||
m_zoomLabel->setText(QStringLiteral("--"));
|
||||
} else {
|
||||
m_zoomLabel->setText(QStringLiteral("%1%")
|
||||
.arg(m_zoomRatio * 100.0, 0, 'f', 1));
|
||||
}
|
||||
m_zoomLabel->adjustSize();
|
||||
repositionZoomLabel();
|
||||
}
|
||||
|
||||
void ImageView::updateZoomRatio(double ratio, bool keepCenter)
|
||||
{
|
||||
if (!hasImage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ratio = qBound(m_minimumZoomRatio, ratio, m_maximumZoomRatio);
|
||||
if (qFuzzyCompare(ratio, m_zoomRatio) && !transform().isIdentity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint viewportCenter = viewport()->rect().center();
|
||||
const QPointF sceneCenter = mapToScene(viewportCenter);
|
||||
resetTransform();
|
||||
scale(ratio, ratio);
|
||||
m_zoomRatio = ratio;
|
||||
if (keepCenter) {
|
||||
centerOn(sceneCenter);
|
||||
}
|
||||
updateZoomLabel();
|
||||
emit zoomRatioChanged(m_zoomRatio);
|
||||
}
|
||||
|
||||
void ImageView::repositionZoomLabel()
|
||||
{
|
||||
if (!m_zoomLabel) {
|
||||
return;
|
||||
}
|
||||
const int margin = 8;
|
||||
const int scrollBarHeight = horizontalScrollBar()->isVisible()
|
||||
? horizontalScrollBar()->height()
|
||||
: 0;
|
||||
m_zoomLabel->move(margin,
|
||||
height() - m_zoomLabel->height() - margin - scrollBarHeight);
|
||||
m_zoomLabel->raise();
|
||||
}
|
||||
#include "ImageView.h"
|
||||
|
||||
#include <QGraphicsPixmapItem>
|
||||
#include <QGraphicsScene>
|
||||
#include <QLabel>
|
||||
#include <QMouseEvent>
|
||||
#include <QResizeEvent>
|
||||
#include <QtWidgets/QScrollBar>
|
||||
#include <QStyle>
|
||||
#include <QWheelEvent>
|
||||
#include <QtMath>
|
||||
|
||||
namespace {
|
||||
constexpr double kDefaultMinimumZoom = 0.02;
|
||||
constexpr double kDefaultMaximumZoom = 20.0;
|
||||
constexpr double kZoomStep = 1.25;
|
||||
}
|
||||
|
||||
ImageView::ImageView(QWidget* parent)
|
||||
: QGraphicsView(parent),
|
||||
m_scene(new QGraphicsScene(this)),
|
||||
m_pixmapItem(nullptr),
|
||||
m_zoomLabel(new QLabel(this)),
|
||||
m_zoomRatio(1.0),
|
||||
m_minimumZoomRatio(kDefaultMinimumZoom),
|
||||
m_maximumZoomRatio(kDefaultMaximumZoom),
|
||||
m_imageAlignment(Qt::AlignCenter),
|
||||
m_panning(false),
|
||||
m_needsInitialFit(false)
|
||||
{
|
||||
setScene(m_scene);
|
||||
setBackgroundBrush(QColor(35, 35, 35));
|
||||
setFrameShape(QFrame::NoFrame);
|
||||
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
|
||||
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
|
||||
setResizeAnchor(QGraphicsView::AnchorViewCenter);
|
||||
setAlignment(m_imageAlignment);
|
||||
setDragMode(QGraphicsView::NoDrag);
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
|
||||
m_zoomLabel->setObjectName(QStringLiteral("imageViewZoomLabel"));
|
||||
m_zoomLabel->setAlignment(Qt::AlignCenter);
|
||||
m_zoomLabel->setMinimumWidth(58);
|
||||
m_zoomLabel->setAttribute(Qt::WA_TransparentForMouseEvents);
|
||||
m_zoomLabel->setStyleSheet(QStringLiteral(
|
||||
"QLabel { color: white; background: transparent; border: none; }"));
|
||||
updateZoomLabel();
|
||||
}
|
||||
|
||||
ImageView::~ImageView() = default;
|
||||
|
||||
void ImageView::setImage(const QImage& image)
|
||||
{
|
||||
if (image.isNull()) {
|
||||
clearImage();
|
||||
return;
|
||||
}
|
||||
|
||||
m_image = image;
|
||||
if (!m_pixmapItem) {
|
||||
m_pixmapItem = m_scene->addPixmap(QPixmap::fromImage(m_image));
|
||||
} else {
|
||||
m_pixmapItem->setPixmap(QPixmap::fromImage(m_image));
|
||||
}
|
||||
m_scene->setSceneRect(QRectF(QPointF(0.0, 0.0), QSizeF(m_image.size())));
|
||||
m_needsInitialFit = true;
|
||||
if (viewport()->width() > 0 && viewport()->height() > 0) {
|
||||
m_needsInitialFit = false;
|
||||
resetView();
|
||||
}
|
||||
emit imageChanged();
|
||||
}
|
||||
|
||||
void ImageView::setImage(const QPixmap& pixmap)
|
||||
{
|
||||
setImage(pixmap.toImage());
|
||||
}
|
||||
|
||||
bool ImageView::loadImage(const QString& filePath)
|
||||
{
|
||||
QImage loaded;
|
||||
if (!loaded.load(filePath)) {
|
||||
return false;
|
||||
}
|
||||
setImage(loaded);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageView::clearImage()
|
||||
{
|
||||
m_image = QImage();
|
||||
m_scene->clear();
|
||||
m_pixmapItem = nullptr;
|
||||
m_scene->setSceneRect(QRectF());
|
||||
m_zoomRatio = 1.0;
|
||||
m_needsInitialFit = false;
|
||||
updateZoomLabel();
|
||||
viewport()->update();
|
||||
emit imageChanged();
|
||||
}
|
||||
|
||||
bool ImageView::hasImage() const
|
||||
{
|
||||
return !m_image.isNull();
|
||||
}
|
||||
|
||||
QImage ImageView::image() const
|
||||
{
|
||||
return m_image;
|
||||
}
|
||||
|
||||
double ImageView::zoomRatio() const
|
||||
{
|
||||
return m_zoomRatio;
|
||||
}
|
||||
|
||||
void ImageView::setZoomRatio(double ratio)
|
||||
{
|
||||
updateZoomRatio(ratio, true);
|
||||
}
|
||||
|
||||
void ImageView::zoomIn(double factor)
|
||||
{
|
||||
if (factor <= 1.0) {
|
||||
factor = kZoomStep;
|
||||
}
|
||||
updateZoomRatio(m_zoomRatio * factor, true);
|
||||
}
|
||||
|
||||
void ImageView::zoomOut(double factor)
|
||||
{
|
||||
if (factor <= 1.0) {
|
||||
factor = kZoomStep;
|
||||
}
|
||||
updateZoomRatio(m_zoomRatio / factor, true);
|
||||
}
|
||||
|
||||
void ImageView::resetView()
|
||||
{
|
||||
if (!hasImage() || !m_pixmapItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const double previousRatio = m_zoomRatio;
|
||||
resetTransform();
|
||||
fitInView(m_pixmapItem, Qt::KeepAspectRatio);
|
||||
|
||||
const double fittedRatio = qBound(m_minimumZoomRatio,
|
||||
transform().m11(),
|
||||
m_maximumZoomRatio);
|
||||
resetTransform();
|
||||
scale(fittedRatio, fittedRatio);
|
||||
m_zoomRatio = fittedRatio;
|
||||
centerOn(m_pixmapItem);
|
||||
updateZoomLabel();
|
||||
|
||||
if (!qFuzzyCompare(previousRatio, m_zoomRatio)) {
|
||||
emit zoomRatioChanged(m_zoomRatio);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageView::setZoomRange(double minimum, double maximum)
|
||||
{
|
||||
if (minimum <= 0.0 || maximum < minimum) {
|
||||
return;
|
||||
}
|
||||
m_minimumZoomRatio = minimum;
|
||||
m_maximumZoomRatio = maximum;
|
||||
updateZoomRatio(m_zoomRatio, true);
|
||||
}
|
||||
|
||||
double ImageView::minimumZoomRatio() const
|
||||
{
|
||||
return m_minimumZoomRatio;
|
||||
}
|
||||
|
||||
double ImageView::maximumZoomRatio() const
|
||||
{
|
||||
return m_maximumZoomRatio;
|
||||
}
|
||||
|
||||
void ImageView::setImageAlignment(Qt::Alignment alignment)
|
||||
{
|
||||
if (!alignment) {
|
||||
alignment = Qt::AlignCenter;
|
||||
}
|
||||
m_imageAlignment = alignment;
|
||||
setAlignment(m_imageAlignment);
|
||||
if (hasImage()) {
|
||||
viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
Qt::Alignment ImageView::imageAlignment() const
|
||||
{
|
||||
return m_imageAlignment;
|
||||
}
|
||||
|
||||
void ImageView::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QGraphicsView::resizeEvent(event);
|
||||
if (m_needsInitialFit && hasImage() && viewport()->width() > 0 && viewport()->height() > 0) {
|
||||
m_needsInitialFit = false;
|
||||
resetView();
|
||||
}
|
||||
repositionZoomLabel();
|
||||
}
|
||||
|
||||
void ImageView::wheelEvent(QWheelEvent* event)
|
||||
{
|
||||
if (!hasImage() || event->angleDelta().y() == 0) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
|
||||
const double factor = event->angleDelta().y() > 0 ? kZoomStep : 1.0 / kZoomStep;
|
||||
updateZoomRatio(m_zoomRatio * factor, true);
|
||||
event->accept();
|
||||
}
|
||||
|
||||
void ImageView::mousePressEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && hasImage()) {
|
||||
m_panning = true;
|
||||
m_lastMousePosition = event->pos();
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseMoveEvent(QMouseEvent* event)
|
||||
{
|
||||
if (m_panning) {
|
||||
const QPoint delta = event->pos() - m_lastMousePosition;
|
||||
m_lastMousePosition = event->pos();
|
||||
horizontalScrollBar()->setValue(horizontalScrollBar()->value() - delta.x());
|
||||
verticalScrollBar()->setValue(verticalScrollBar()->value() - delta.y());
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseReleaseEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && m_panning) {
|
||||
m_panning = false;
|
||||
setCursor(Qt::OpenHandCursor);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseReleaseEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::mouseDoubleClickEvent(QMouseEvent* event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton && hasImage()) {
|
||||
resetView();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QGraphicsView::mouseDoubleClickEvent(event);
|
||||
}
|
||||
|
||||
void ImageView::updateZoomLabel()
|
||||
{
|
||||
if (!hasImage()) {
|
||||
m_zoomLabel->setText(QStringLiteral("--"));
|
||||
} else {
|
||||
m_zoomLabel->setText(QStringLiteral("%1%")
|
||||
.arg(m_zoomRatio * 100.0, 0, 'f', 1));
|
||||
}
|
||||
m_zoomLabel->adjustSize();
|
||||
repositionZoomLabel();
|
||||
}
|
||||
|
||||
void ImageView::updateZoomRatio(double ratio, bool keepCenter)
|
||||
{
|
||||
if (!hasImage()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ratio = qBound(m_minimumZoomRatio, ratio, m_maximumZoomRatio);
|
||||
if (qFuzzyCompare(ratio, m_zoomRatio) && !transform().isIdentity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QPoint viewportCenter = viewport()->rect().center();
|
||||
const QPointF sceneCenter = mapToScene(viewportCenter);
|
||||
resetTransform();
|
||||
scale(ratio, ratio);
|
||||
m_zoomRatio = ratio;
|
||||
if (keepCenter) {
|
||||
centerOn(sceneCenter);
|
||||
}
|
||||
updateZoomLabel();
|
||||
emit zoomRatioChanged(m_zoomRatio);
|
||||
}
|
||||
|
||||
void ImageView::repositionZoomLabel()
|
||||
{
|
||||
if (!m_zoomLabel) {
|
||||
return;
|
||||
}
|
||||
const int margin = 8;
|
||||
const int scrollBarHeight = horizontalScrollBar()->isVisible()
|
||||
? horizontalScrollBar()->height()
|
||||
: 0;
|
||||
m_zoomLabel->move(margin,
|
||||
height() - m_zoomLabel->height() - margin - scrollBarHeight);
|
||||
m_zoomLabel->raise();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user