style:调整文件结束符

This commit is contained in:
YangSn 2026-10-10 19:02:17 +08:00
parent 2ee6bcd414
commit 7b84fffd2a
15 changed files with 3386 additions and 3386 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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, &params](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, &params](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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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

View File

@ -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;
};

View File

@ -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();
}