Что происходит с размером STEP-файла?
Удивительная история приключилась на днях с конвертацией данных.
Модель дизельного двигателя Branco BDA. |
При чтении и последующей перезаписи произвольно взятой модели STEP-транслятором библиотеки OpenCascade, размер файла увеличился с 96 до 196 мегабайт. Изменение размера как таковое логично, поскольку данный файл не был произведен библиотекой OpenCascade (по всей видимости ядром являлся Parasolid), а в качестве конвертера выступали инструменты STEP Tools, но... Сто мегабайт сверху!
Выяснилось, что оригинальный STEP содержит ~1,285 тыс. объектов, тогда как в перезаписаном файле объектов стало ~2,248 тыс. Возникает вопрос: что это за дополнительный миллион (!) непрошенных сущностей?
Количество объектов в оригинальном файле. |
Количество объектов в заново сохраненном файле. |
Начнем с того, как узнать количество объектов в файле STEP. Для этого можно заглянуть непосредственно в файл (благо, он читабелен) и найти в нем самый старший идентификатор. Другой способ состоит в том, чтобы узнать количество объектов программно. Сделать это можно после чтения файла и перед его трансляцией в топологическую модель OpenCascade. Вот пример:
STEPControl_Reader reader; // Read file. if ( reader.ReadFile( filename.ToCString() ) == IFSelect_RetDone ) { // Get STEP model. Handle(StepData_StepModel) stepModel = reader.StepModel(); Interface_EntityIterator entIt = stepModel->Entities(); // Print number of entities. std::cout << "Read " << entIt.NbEntities() " << entities from STEP file." << std::endl; }
Обратите внимание на структуру данных StepData_StepModel. Она содержит объектную модель файла STEP как граф взаимосвязей между различными сущностями. В этой модели нет реальной геометрии CAD, а есть только ее описание, зеркалирующее состав оригинального файла. На следующем этапе содержимое StepData_StepModel будет интерпретироваться транслятором для воссоздания геометрической модели с ассоциированными метаданными.
// Transfer all roots into one shape or into several shapes. try { reader.TransferRoots(); } catch ( Standard_Failure ) { std::cout << "Warning: exception occurred during translation." << std::endl; } if ( reader.NbShapes() <= 0 ) { std::cout << "Error: transferring STEP to BREP failed." << std::endl; return false; } TopoDS_Shape result = reader.OneShape();
После трансляции можно проверить состав CAD-модели "result" уже в терминах ее реально воссозданных граничных элементов.
Количество топологических объектов в оригинальном файле. |
Существенной разницы между двумя файлами эта проверка не выявляет.
Количество топологических объектов в заново сохраненном файле. |
Используя программный инструментарий, нетрудно собрать полную информацию о количестве и составе объектов STEP-модели еще до этапа трансляции. Ниже выведен дамп итоговой модели (той, которую «разнесло»):
StepVisual_PresentationStyleAssignment : 371 StepGeom_Vector : 27132 StepBasic_ProductContext : 1 StepShape_EdgeLoop : 25420 StepGeom_GeomRepContextAndGlobUnitAssCtxAndGlobUncertaintyAssCtx : 1 StepGeom_Ellipse : 1633 StepGeom_Axis1Placement : 1 StepShape_ManifoldSolidBrep : 368 StepBasic_ApplicationContext : 1 StepGeom_Line : 26095 StepShape_FaceBound : 25420 StepGeom_BSplineSurfaceWithKnots : 621 StepVisual_ColourRgb : 6 StepGeom_SurfaceOfRevolution : 1 StepBasic_ApplicationProtocolDefinition : 1 StepShape_ClosedShell : 385 StepGeom_Circle : 14922 StepGeom_SphericalSurface : 543 StepShape_AdvancedFace : 22414 StepGeom_Plane : 9677 StepGeom_BSplineSurfaceWithKnotsAndRationalBSplineSurface : 2691 StepVisual_SurfaceSideStyle : 371 StepGeom_BSplineCurveWithKnots : 15503 StepBasic_ProductRelatedProductCategory : 1 StepBasic_ProductDefinitionContext : 1 StepBasic_UncertaintyMeasureWithUnit : 1 StepShape_ShapeDefinitionRepresentation : 1 StepGeom_Direction : 96373 StepGeom_Axis2Placement3d : 34620 StepRepr_ProductDefinitionShape : 1 StepGeom_SurfaceOfLinearExtrusion : 1037 StepGeom_CartesianPoint : 1723968 StepGeom_ConicalSurface : 1065 StepShape_EdgeCurve : 57458 StepBasic_ProductDefinitionFormation : 1 StepShape_VertexPoint : 36637 StepShape_BrepWithVoids : 3 StepGeom_BSplineCurveWithKnotsAndRationalBSplineCurve : 343 StepBasic_SiUnitAndSolidAngleUnit : 1 StepBasic_ProductDefinition : 1 StepVisual_FillAreaStyleColour : 371 StepBasic_SiUnitAndPlaneAngleUnit : 1 StepVisual_StyledItem : 371 StepVisual_MechanicalDesignGeometricPresentationRepresentation : 1 StepGeom_CylindricalSurface : 5429 StepShape_OrientedEdge : 114916 StepVisual_SurfaceStyleFillArea : 371 StepVisual_PresentationLayerAssignment : 1 StepVisual_SurfaceStyleUsage : 371 StepShape_OrientedClosedShell : 14 StepBasic_SiUnitAndLengthUnit : 1 StepVisual_FillAreaStyle : 371 StepShape_ShapeRepresentation : 1 StepBasic_Product : 1 StepGeom_ToroidalSurface : 1350
Сличая такие дампы для исходного и результирующего файлов, можно заметить несколько интересных вещей. Вот только некоторые наблюдения:
- Исчезли объекты типа StepShape_VertexLoop.
- Все объекты StepShape_FaceOuterBound были преобразованы в StepShape_FaceBound.
- Количество нерациональных B-кривых (StepGeom_BSplineCurveWithKnots) возросло с 11743 до 15503.
- Количество точек в пространстве моделирования (StepGeom_CartesianPoint) возросло с 781,352 до 1,723,968!
- И некоторые другие изменения.
Точки (StepGeom_CartesianPoint), прочитанные из оригинального STEP-файла. |
В данном случае особенно подозрительным выглядит возросшее количество точек (StepGeom_CartesianPoint). Это может быть связано с тем, что какие-то точки, например, дублируются, либо, что менее очевидно, могла увеличиться сложность B-кривых и B-поверхностей. Например, вставка нового узла влечет образование дополнительной контрольной точки в B-кривой. В формате STEP это отразится возникновением новой сущности StepGeom_CartesianPoint вместе со ссылкой на нее из соответствующего определения B-кривой. Но пока это только догадки.
Начнем с проверки «в лоб». Для анализа пространственных точек на совпадение с контролируемой точностью можно использовать класс NCollection_CellFilter библиотеки OpenCascade. Для этого сначала реализуется собственно код проверки, например, вот так (обратите внимание на наследование от NCollection_CellFilter_InspectorXYZ):
//! Auxiliary class to search for coincident spatial points. class InspectXYZ : public NCollection_CellFilter_InspectorXYZ { public: typedef gp_XYZ Target; //! Constructor accepting resolution distance and point. InspectXYZ(const double tol, const gp_XYZ& P) : m_fTol(tol), m_bFound(false), m_P(P) {} //! eturn true/false depending on whether the node was found or not. bool IsFound() const { return m_bFound; } //! Implementation of inspection method. NCollection_CellFilter_Action Inspect(const gp_XYZ& Target) { m_bFound = ( (m_P - Target).SquareModulus() <= Square(m_fTol) ); return CellFilter_Keep; } private: gp_XYZ m_P; //!< Source point. bool m_bFound; //!< Whether two points are coincident or not. double m_fTol; //!< Resolution to check for coincidence. };
Класс InspectXYZ описывает ячейку (cell) в пространстве с центром в некоторой точке "P" и зазором "tol". Метод Inspect() анализирует принятую извне точку на попадание в данную ячейку. Если расстояние между центром ячейки и данной точкой не превосходит значения зазора, то такие точки полагаются одинаковыми. На следующем этапе мы используем класс InspectXYZ для фильтрации точек:
... // Prepare iterator by STEP entities. Interface_EntityIterator entIt = stepModel->Entities(); // Cell filter for Cartesian points. const double conf = 15.0; // NCollection_CellFilter<InspectXYZ> NodeFilter(conf); // Iterate all entities. Handle(asiAlgo_BaseCloud<double>) pts = new asiAlgo_BaseCloud<double>; // for ( ; entIt.More(); entIt.Next() ) { const Handle(Standard_Transient)& ent = entIt.Value(); const Handle(Standard_Type)& entType = ent->DynamicType(); if ( ent->IsKind( STANDARD_TYPE(StepGeom_CartesianPoint) ) ) { Handle(StepGeom_CartesianPoint) cpEnt = Handle(StepGeom_CartesianPoint)::DownCast(ent); Handle(TColStd_HArray1OfReal) coords = cpEnt->Coordinates(); // if ( !coords.IsNull() && coords->Size() == 3 ) { gp_XYZ xyz( coords->Value( coords->Lower() ), coords->Value( coords->Lower() + 1 ), coords->Value( coords->Lower() + 2) ); InspectXYZ Inspect(conf, xyz); gp_XYZ XYZ_min = Inspect.Shift( xyz, -Precision::Confusion() ); gp_XYZ XYZ_max = Inspect.Shift( xyz, Precision::Confusion() ); // Coincidence test. NodeFilter.Inspect(XYZ_min, XYZ_max, Inspect); const bool isFound = Inspect.IsFound(); // if ( !isFound ) { pts->AddElement( xyz.X(), xyz.Y(), xyz.Z() ); NodeFilter.Add(xyz, xyz); } } } }
Здесь asiAlgo_BaseCloud представляет собой облако точек, в которое добавляются только те тройки координат, что выдержали проверку фильтром. Используя эту технику, можно эффективно отсеивать геометрически близкие точки для решения разнообразных задач (например, аппроксимации). Управляя значением переменной "conf", мы контролируем степень разреженности облака. Поскольку в библиотеке OpenCascade совпадающими считаются точки, находящиеся друг от друга на расстоянии не более Precision::Confusion(), то будем использовать именно это значение (равное 1.e-7) для фильтрации. В результате получаем 669,750 точек, то есть более миллиона точек были отфильтрованы как совпадающие с другими. Итак, по всей видимости мы имеем дело с некоторой избыточностью объектов в модели STEP. Постараемся выяснить, а откуда данные точки вообще появились?
В оригинальном файле все B-кривые типа StepGeom_BSplineCurveWithKnots содержали 386,783 контрольных точек. В результирующем файле контрольных точек стало 1,326,619, то есть на миллион больше.
Количество контрольных точек B-кривых до и после трансляции оригинального файла. |
После трансляции CAD-модели, OpenCascade выполняет серию дополнительных действий для преобразования восстановленной геометрии в корректное состояние. Этот процесс необходим для преодоления различий, существующих между геометрическими ядрами. Хотя формат STEP сам по себе является «общим знаменателем» САПР, критерии, предъявляемые CAD-системами к геометрической модели, не вполне совпадают. Системы «разговаривают на разных языках». Технически, трансляция модели в OpenCascade венчается вызовом метода ProcessShape() класса XSAlgo_AlgoContainer.
Количество контрольных точек B-кривых после трансляции оригинального файла с выключенным постпроцессингом. |
Класс XSAlgo_AlgoContainer выполняет, в частности, процедуры «лечения» модели, которые и приводят к повышению сложности геометрии. Отключать эти процедуры нельзя, так как в противном случае результирующая модель окажется непригодной к дальнейшей работе. Следует, однако, разобраться, что же не так с исходной моделью. Или «болеет» сам транслятор? Выясняется, что увеличение сложности ребер связано с работой класса ShapeFix_Face и его двух режимов:
- FixWireMode
- FixMissingSeamMode
Эти режимы позволяют вставить недостающие шовные ребра и соответствующим образом преобразовать контуры грани.
Одна из «больных» граней до обработки в ShapeFix_Face. |
Согласно критериям корректности моделей в OpenCascade, любая грань должна иметь замкнутый контур в своем параметрическом пространстве. Шовное ребро — это ребро, проходящее через период несущей поверхности. В пространстве моделирования ему отвечает единственная кривая, тогда как в 2D оно имеет две сопряженные параметрические кривые. В исходном STEP-файле может не быть как самих шовных ребер (в силу особенностей геометрического ядра), так и параметрических кривых вообще. Восстановление этих недостающих объектов есть прямая обязанность пост-процессора OpenCascade.
Та же грань после обработки в ShapeFix_Face. |
Оказывается, что причиной появления миллиона новых точек является процесс восстановления шовного ребра. Шовное ребро разбивает существующие ребра на сегменты для образования новых контуров. Однако полученные сегменты переиспользуют в точности ту же несущую кривую, что и оригинальное ребро, претерпевающее разбиение. В результате там, где можно было обойтись лишь небольшим участком B-кривой, мы имеем ее полностью, со всеми контрольными точками и узловыми векторами.
Кривые, разбитые восстановленным шовным ребром, дублируются вместе со всеми контрольными точками. Слева выбранное ребро отмечено желтым цветом. Справа показана соответствующая несущая кривая, которая оказывается избыточной для моделирования данного ребра. |
Понятно, что копирование B-кривой означает, в частности, копирование ее контрольных точек. Именно поэтому мы и наблюдаем появление большого числа геометрически идентичных точек, которые, будучи записанными как отдельные объекты в STEP, дают заметное увеличение размера файла. В завершении отметим, что по результатам данного анализа был подготовлен отчет в официальный багтрекер. Давайте вместе делать ядро лучше.