-
-
Save Eisenwave/799416ac162a4dddeb1312f357f1385c to your computer and use it in GitHub Desktop.
Voxel Writer for SVX file format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "svx.hpp" | |
#include "3rd_miniz.hpp" | |
#include "core_util/log.hpp" | |
#include "core_util/string.hpp" | |
#include <QBuffer> | |
#include <QImage> | |
#include <QImageWriter> | |
namespace voxelio::svx { | |
static constexpr const char *xmlNameOf(ChannelType type) | |
{ | |
switch (type) { | |
case ChannelType::DENSITY: return "DENSITY"; | |
case ChannelType::COLOR_RGB: return "COLOR-RGB"; | |
case ChannelType::MATERIAL: return "MATERIAL"; | |
case ChannelType::CUSTOM: return "CUSTOM"; | |
} | |
DEBUG_ASSERT_UNREACHABLE(); | |
} | |
static constexpr bool usesId(ChannelType type) | |
{ | |
return type == ChannelType::MATERIAL || type == ChannelType::CUSTOM; | |
} | |
std::string Channel::fullFileNameFormat() const | |
{ | |
return mve::str::to_lower_case<char>(xmlNameOf(type)) + '/' + fileNameFormat; | |
} | |
std::string Channel::toXml() const | |
{ | |
std::stringstream result; | |
result << "<channel "; | |
result << "type=\"" << xmlNameOf(type); | |
if (usesId(type)) { | |
result << "(" << id << ")"; | |
} | |
result << "\" bits=\"" << bits; | |
result << "\" slices=\"" << fullFileNameFormat(); | |
result << "\"/>"; | |
return result.str(); | |
} | |
std::string Manifest::toXml() const | |
{ | |
std::stringstream result; | |
result << "<?xml version=\"1.0\"?>"; | |
result << "<grid version=\"1.0\" "; | |
result << "gridSizeX=\"" << gridSize_.x() << "\" "; | |
result << "gridSizeY=\"" << gridSize_.y() << "\" "; | |
result << "gridSizeZ=\"" << gridSize_.z() << "\" "; | |
result << "voxelSize=\"" << voxelSize_ << "\" "; | |
result << "subvoxelBits=\"" << subvoxelBits_ << "\" "; | |
result << "slicesOrientation=\"" << nameOf(slicesOrientation_) << "\">"; | |
if (not channels.empty()) { | |
result << "<channels>"; | |
for (const auto &channel : channels) { | |
result << channel.toXml(); | |
} | |
result << "</channels>"; | |
} | |
result << "<materials></materials>"; | |
result << "<metadata></metadata>"; | |
result << "</grid>"; | |
return result.str(); | |
} | |
bool Manifest::matches(const mve::VoxelArray voxels) const | |
{ | |
return gridSize().x() >= voxels.sizeX && gridSize().y() >= voxels.sizeY && gridSize().z() >= voxels.sizeZ; | |
} | |
struct SvxLimits { | |
size_t slice; | |
size_t u; | |
size_t v; | |
}; | |
/** | |
* Transforms world space coordinates into slice, u, v coordinates. | |
*/ | |
template <mve::Axis AXIS> | |
static constexpr SvxLimits svxLimitsOf(Vec3size xyzLimits) | |
{ | |
if constexpr (AXIS == mve::Axis::X) { | |
return {xyzLimits.x(), xyzLimits.y(), xyzLimits.z()}; | |
} | |
else if constexpr (AXIS == mve::Axis::Y) { | |
return {xyzLimits.y(), xyzLimits.x(), xyzLimits.z()}; | |
} | |
else { | |
return {xyzLimits.z(), xyzLimits.x(), xyzLimits.y()}; | |
} | |
} | |
/** | |
* Transforms slice, u, v coordinates into world space. | |
*/ | |
template <mve::Axis AXIS> | |
constexpr Vec3size svxProject(Vec3size suv) | |
{ | |
if constexpr (AXIS == mve::Axis::X) { | |
return Vec3size(suv[0], suv[1], suv[2]); | |
} | |
else if constexpr (AXIS == mve::Axis::Y) { | |
return Vec3size(suv[1], suv[0], suv[2]); | |
} | |
else { | |
return Vec3size(suv[1], suv[2], suv[0]); | |
} | |
} | |
static QImage::Format densityFormatOf(const size_t bits) | |
{ | |
switch (bits) { | |
case 1: return QImage::Format::Format_Mono; | |
case 8: return QImage::Format::Format_Grayscale8; | |
// case 16: return QImage::Format::Format_Grayscale16; only available in 5.13 | |
default: return QImage::Format::Format_Invalid; | |
} | |
} | |
static QImage::Format rgbFormatOf(const size_t bits) | |
{ | |
switch (bits) { | |
case 1: return QImage::Format::Format_Mono; | |
case 12: return QImage::Format::Format_RGB444; | |
case 15: return QImage::Format::Format_RGB555; | |
case 16: return QImage::Format::Format_RGB16; | |
case 18: return QImage::Format::Format_RGB666; | |
case 24: return QImage::Format::Format_RGB888; | |
case 30: return QImage::Format::Format_RGB30; | |
case 32: return QImage::Format::Format_RGBX8888; | |
default: return QImage::Format::Format_Invalid; | |
} | |
} | |
static QImage::Format formatOf(const Channel &channel) | |
{ | |
switch (channel.type) { | |
case ChannelType::DENSITY: return densityFormatOf(channel.bits); | |
case ChannelType::COLOR_RGB: return rgbFormatOf(channel.bits); | |
default: return QImage::Format_Invalid; | |
} | |
} | |
static QColor qcolorof(RGB32 rgb) | |
{ | |
return QColor{rgb.r(), rgb.g(), rgb.b(), rgb.a()}; | |
} | |
using RgbEncoder = QRgb (*)(RGB32 rgb); | |
static RgbEncoder encoderOf(QImage::Format format) | |
{ | |
switch (format) { | |
case QImage::Format_Mono: | |
return [](RGB32 rgb) -> QRgb { | |
return rgb.a() != 0; | |
}; | |
case QImage::Format_Grayscale8: | |
return [](RGB32 rgb) -> QRgb { | |
return rgb.a(); | |
}; | |
case QImage::Format_RGB444: | |
case QImage::Format_RGB555: | |
case QImage::Format_RGB16: | |
case QImage::Format_RGB666: | |
case QImage::Format_RGB888: | |
case QImage::Format_RGB30: | |
case QImage::Format_RGBX8888: | |
return [](RGB32 rgb) -> QRgb { | |
return qcolorof(rgb).rgb(); | |
}; | |
default: return nullptr; | |
} | |
} | |
template <mve::Axis AXIS> | |
static void writeSvxLayerToImage( | |
QImage &image, const mve::VoxelArray &voxels, const SvxLimits &lims, size_t slice, QImage::Format format) | |
{ | |
auto encoder = encoderOf(format); | |
DEBUG_ASSERT_NOTNULL(encoder); | |
for (size_t u = 0; u < lims.u; ++u) { | |
for (size_t v = 0; v < lims.v; ++v) { | |
Vec3size xyz = svxProject<AXIS>({slice, u, v}); | |
RGB32 rgb = voxels.getRGB(xyz); | |
image.setPixel(static_cast<int>(u), static_cast<int>(v), encoder(rgb)); | |
} | |
} | |
} | |
template <mve::Axis AXIS> | |
ResultCode Serializer::writeChannel(miniz_cpp::zip_file &archive, const SimpleVoxels &svx, const Channel &channel) | |
{ | |
if (channel.type == ChannelType::CUSTOM || channel.type == ChannelType::MATERIAL) { | |
err = {0, "Writing CUSTOM or MATERIAL channels is not supported"}; | |
return ResultCode::WRITE_ERROR_UNSUPPORTED_FORMAT; | |
} | |
std::string fileNameFormat = channel.fullFileNameFormat(); | |
const char *fileNameFormatCstr = fileNameFormat.c_str(); | |
const mve::VoxelArray &voxels = svx.voxels; | |
const SvxLimits lims = svxLimitsOf<AXIS>({voxels.sizeX, voxels.sizeY, voxels.sizeZ}); | |
const QImage::Format format = formatOf(channel); | |
if (format == QImage::Format_Invalid) { | |
err = {0, "Can't determine valid image format for the " + std::string{xmlNameOf(channel.type)} + " channel"}; | |
return ResultCode::USER_ERROR_INVALID_COLOR_FORMAT; | |
} | |
const QImage baseImage{static_cast<int>(lims.u), static_cast<int>(lims.v), format}; | |
QByteArray byteArray; | |
QBuffer buffer{&byteArray}; | |
QImageWriter writer{&buffer, "png"}; | |
writer.setQuality(100); | |
ALWAYS_ASSERT(writer.canWrite()); | |
for (size_t slice = 0; slice < lims.slice; ++slice) { | |
std::string fileName = mve::str::format(fileNameFormatCstr, slice); | |
QImage sliceImage = baseImage; | |
LOG(SPAM, "Writing slice #" + std::to_string(slice) + ", named" + fileName); | |
writeSvxLayerToImage<AXIS>(sliceImage, voxels, lims, slice, format); | |
buffer.close(); | |
writer.write(sliceImage); | |
archive.writestr(fileName, {byteArray.data(), static_cast<size_t>(byteArray.size())}); | |
} | |
return ResultCode::OK; | |
} | |
ResultCode Serializer::toStream(std::ostream &stream, const SimpleVoxels &svx) | |
{ | |
miniz_cpp::zip_file archive; | |
archive.writestr("manifest.xml", svx.manifest.toXml()); | |
const mve::Axis axis = svx.manifest.slicesOrientation(); | |
for (const Channel &channel : svx.manifest.channels) { | |
ResultCode code; | |
// here we constexpr-ify the axis parameter | |
// this allows for compiler optimizations and cleaner code further down the line | |
switch (axis) { | |
case mve::Axis::X: code = writeChannel<mve::Axis::X>(archive, svx, channel); break; | |
case mve::Axis::Y: code = writeChannel<mve::Axis::Y>(archive, svx, channel); break; | |
case mve::Axis::Z: code = writeChannel<mve::Axis::Z>(archive, svx, channel); break; | |
} | |
if (code != ResultCode::OK) { | |
return code; | |
} | |
} | |
archive.save(stream); | |
if (not stream.good()) { | |
err = {static_cast<u64>(stream.tellp()), "Stream was not left in a good() state after writing file"}; | |
return ResultCode::WRITE_ERROR_IO_FAIL; | |
} | |
return ResultCode::OK; | |
} | |
} // namespace voxelio::svx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef SVX_HPP | |
#define SVX_HPP | |
#include "3rd_minizfwd.hpp" | |
#include "types.hpp" | |
#include "voxelio.hpp" | |
#include "core_structs/voxelarray.hpp" | |
#include "core_util/macro.hpp" | |
#include "core_util/vec.hpp" | |
namespace voxelio::svx { | |
enum class ChannelType { | |
/** Represents an approximation to the percent of voxel fill. */ | |
DENSITY, | |
/** COLOR – RGB. */ | |
COLOR_RGB, | |
/** Fraction of amount a given material used in a voxel (all densities must sum to 100% or 0%). */ | |
MATERIAL, | |
/** Custom value without defined semantic meaning. */ | |
CUSTOM | |
}; | |
struct Channel { | |
/** The type of information stored in this channel. */ | |
ChannelType type; | |
/** The number of bits used for the channel, default is 8 bits. */ | |
size_t bits = 0; | |
/** Optional: The material or custom id of this channel. Not necessary for density or color. */ | |
size_t id = 0; | |
/** The printf-pattern for the paths of slices belonging to this channel. */ | |
std::string fileNameFormat = "%04d.png"; | |
std::string fullFileNameFormat() const; | |
std::string toXml() const; | |
}; | |
/** A struct representing the manifest of an SVX file. */ | |
class Manifest { | |
public: | |
/** The specification version this manifest follows. */ | |
static constexpr const char *VERSION = "1.0"; | |
private: | |
/** The number of voxels in each direction. */ | |
Vec3size gridSize_; | |
/** The origin of the grid in physical space in meters. */ | |
Vec3f origin_ = {0, 0, 0}; | |
/** The axis(X,Y,Z) orthogonal to the slices plane. */ | |
mve::Axis slicesOrientation_; | |
/** Number of bits used per voxel for density (1-16). */ | |
uint32_t subvoxelBits_; | |
/** The physical size of each voxel in meters. */ | |
float voxelSize_; | |
public: | |
std::vector<Channel> channels{}; | |
Manifest(Vec3size gridSize, | |
Vec3f origin = {0, 0, 0}, | |
mve::Axis slicesOrientation = mve::Axis::Y, | |
uint32_t subvoxelBits = 1, | |
float voxelSize = 1 / 16.f) | |
: gridSize_{std::move(gridSize)} | |
, origin_{origin} | |
, slicesOrientation_{slicesOrientation} | |
, subvoxelBits_{subvoxelBits} | |
, voxelSize_{voxelSize} | |
{ | |
ALWAYS_ASSERT_NE(this->gridSize_.x(), 0); | |
ALWAYS_ASSERT_NE(this->gridSize_.y(), 0); | |
ALWAYS_ASSERT_NE(this->gridSize_.z(), 0); | |
ALWAYS_ASSERT(subvoxelBits > 0 && subvoxelBits <= 16); | |
} | |
DEFAULT_MOVE_AND_COPY_CONSTRUCTORS_AND_ASSIGNMENTS(Manifest) | |
constexpr Vec3size gridSize() const | |
{ | |
return gridSize_; | |
} | |
constexpr Vec3f origin() const | |
{ | |
return origin_; | |
} | |
constexpr mve::Axis slicesOrientation() const | |
{ | |
return slicesOrientation_; | |
} | |
constexpr uint32_t subvoxelBits() const | |
{ | |
return subvoxelBits_; | |
} | |
constexpr float voxelSize() const | |
{ | |
return voxelSize_; | |
} | |
bool matches(const mve::VoxelArray voxels) const; | |
std::string toXml() const; | |
}; | |
struct SimpleVoxels { | |
Manifest manifest; | |
mve::VoxelArray voxels; | |
SimpleVoxels(Manifest manifest, mve::VoxelArray voxels) : manifest{std::move(manifest)}, voxels{std::move(voxels)} | |
{ | |
ALWAYS_ASSERT(this->manifest.matches(this->voxels)); | |
} | |
}; | |
class Serializer : public AbstractSerializer { | |
public: | |
ResultCode toStream(std::ostream &stream, const SimpleVoxels &svx) noexcept(false); | |
private: | |
template <mve::Axis AXIS> | |
ResultCode writeChannel(miniz_cpp::zip_file &archive, const SimpleVoxels &svx, const Channel &channel); | |
}; | |
} // namespace voxelio::svx | |
#endif // SVX_HPP |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment