Skip to content
Snippets Groups Projects
Commit a2616ddb authored by Jakub Kolman's avatar Jakub Kolman
Browse files

[#123] feat: procrustes visitor superimposition of points

parent c5d994f1
No related branches found
No related tags found
No related merge requests found
...@@ -22,7 +22,6 @@ import javax.vecmath.Vector3d; ...@@ -22,7 +22,6 @@ import javax.vecmath.Vector3d;
import cz.fidentis.analyst.procrustes.ProcrustesAnalysisFaceModel; import cz.fidentis.analyst.procrustes.ProcrustesAnalysisFaceModel;
import java.util.Comparator; import java.util.Comparator;
/** /**
* @author Jakub Kolman * @author Jakub Kolman
*/ */
...@@ -49,31 +48,31 @@ public class ProcrustesAnalysis { ...@@ -49,31 +48,31 @@ public class ProcrustesAnalysis {
ProcrustesAnalysisFaceModel model1; ProcrustesAnalysisFaceModel model1;
ProcrustesAnalysisFaceModel model2; ProcrustesAnalysisFaceModel model2;
if (humanFace1.getFeaturePoints().size() != humanFace2.getFeaturePoints().size() if (humanFace1.getFeaturePoints().size() != humanFace2.getFeaturePoints().size()
|| !checkFeaturePointsType( || !checkFeaturePointsType(
sortListByFeaturePointType(humanFace1.getFeaturePoints()), sortListByFeaturePointType(humanFace1.getFeaturePoints()),
sortListByFeaturePointType(humanFace1.getFeaturePoints()))) { sortListByFeaturePointType(humanFace1.getFeaturePoints()))) {
// int n = 0; // int n = 0;
Object[] options = {"Yes", "No"}; Object[] options = {"Yes", "No"};
int n = JOptionPane.showOptionDialog( int n = JOptionPane.showOptionDialog(
new JFrame("Warning"), new JFrame("Warning"),
"The sets of feature points do not correspond with each other. Do you want to continue with a subset of feature points?", "The sets of feature points do not correspond with each other. Do you want to continue with a subset of feature points?",
"Warning", "Warning",
JOptionPane.YES_NO_OPTION, JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE, JOptionPane.QUESTION_MESSAGE,
null, null,
options, options,
options[1]); options[1]);
if (n == 0) { if (n == 0) {
ArrayList<Integer> viablePoints = getSubsetOfFeaturePoints(humanFace1.getFeaturePoints(), humanFace2.getFeaturePoints()); ArrayList<Integer> viablePoints = getSubsetOfFeaturePoints(humanFace1.getFeaturePoints(), humanFace2.getFeaturePoints());
model1 = new ProcrustesAnalysisFaceModel(humanFace1, viablePoints); model1 = new ProcrustesAnalysisFaceModel(humanFace1, viablePoints);
model2 = new ProcrustesAnalysisFaceModel(humanFace2, viablePoints); model2 = new ProcrustesAnalysisFaceModel(humanFace2, viablePoints);
Logger.print("Yes"); Logger.print("Yes");
Logger.print(Integer.valueOf(n).toString()); Logger.print(Integer.valueOf(n).toString());
} else { } else {
JOptionPane.showMessageDialog(new JFrame("Procrustes cancelled"), JOptionPane.showMessageDialog(new JFrame("Procrustes cancelled"),
"Lists of feature points do not have the same size and work on subset was cancelled"); "Lists of feature points do not have the same size and work on subset was cancelled");
throw new DataFormatException("Lists of feature points do not have the same size and work on subset was cancelled"); throw new DataFormatException("Lists of feature points do not have the same size and work on subset was cancelled");
} }
...@@ -90,8 +89,8 @@ public class ProcrustesAnalysis { ...@@ -90,8 +89,8 @@ public class ProcrustesAnalysis {
} }
/** /**
* Constructor with variable for scaling option. * Constructor with variable for scaling option. Default scale option is set
* Default scale option is set to false. * to false.
* *
* @param humanFace1 * @param humanFace1
* @param humanFace2 * @param humanFace2
...@@ -107,45 +106,46 @@ public class ProcrustesAnalysis { ...@@ -107,45 +106,46 @@ public class ProcrustesAnalysis {
} }
/** /**
* Method called for analysis after creating initial data in constructor. This method causes superimposition * Method called for analysis after creating initial data in constructor.
* and rotation of the faces. * This method causes superimposition and rotation of the faces.
*
* Using generalized orthogonal procrustes analysis need to be first moved over each other, then scale and then rotate.
* *
* @throws DataFormatException if faces have less than 3 feature points in which case analysis doesn't make sense * Using generalized orthogonal procrustes analysis need to be first moved
* over each other, then scale and then rotate.
*
* @throws DataFormatException if faces have less than 3 feature points in
* which case analysis doesn't make sense
*/ */
public ProcrustesTransformation analyze() throws DataFormatException { public ProcrustesTransformation analyze() throws DataFormatException {
ProcrustesTransformation transformation = new ProcrustesTransformation(); ProcrustesTransformation transformation = new ProcrustesTransformation();
if (this.faceModel1.getFeaturePointsMap().size() >= 3) { if (this.faceModel1.getFeaturePointsMap().size() >= 3) {
// adjustment is used for superimposition and return to the original place // adjustment is used for superimposition and return to the original place
transformation.setCentroidAdjustment(new Vector3d(-this.modelCentroid2.x, -this.modelCentroid2.y, -this.modelCentroid2.z)); transformation.setCentroidAdjustment(new Vector3d(-this.modelCentroid2.x, -this.modelCentroid2.y, -this.modelCentroid2.z));
transformation.setSuperImpositionAdjustment(new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z)); transformation.setSuperImpositionAdjustment(new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z));
// sets both faces to the origin of plains // sets both faces to the origin of plains
superImpose(); superImpose();
// calculation of scaling vector // calculation of scaling vector
if (scale) { if (scale) {
double scaleFactorValue = this.calculateScalingValue(); double scaleFactorValue = this.calculateScalingValue();
if (scaleFactorValue != 1) { if (scaleFactorValue != 1) {
scaleFace(faceModel2, scaleFactorValue); // scaleFace(faceModel2, scaleFactorValue);
calculateScaledList(this.faceModel2.getFeaturePointValues(), scaleFactorValue);
transformation.setScale(scaleFactorValue); transformation.setScale(scaleFactorValue);
} }
} }
// calculation of rotation matrix // calculation of rotation matrix
transformation.setRotationMatrix(this.rotate()); transformation.setRotationMatrix(this.rotate());
// move faces back to so the centroid of vertices is in the origin point // move faces back to so the centroid of vertices is in the origin point
moveFaceModel(faceModel1, new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z)); moveFaceModel(faceModel1, new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z));
moveFaceModel(faceModel2, new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z)); // moveFaceModel(faceModel2, new Vector3d(this.modelCentroid2.x, this.modelCentroid2.y, this.modelCentroid2.z));
// readjustFace(faceModel2,
// new Vector3d(this.modelCentroid2.x, this.modelCentroid2.y, this.modelCentroid2.z),
// new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z));
moveFaceModel(faceModel2,
new Vector3d(this.modelCentroid2.x, this.modelCentroid2.y, this.modelCentroid2.z),
new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z));
// move featurepoints stored in map to superimposed true position // move featurepoints stored in map to superimposed true position
// moveFeaturePoints(faceModel2, new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z)); // moveFeaturePoints(faceModel2, new Vector3d(this.modelCentroid1.x, this.modelCentroid1.y, this.modelCentroid1.z));
transformation.setFaceModel(faceModel2); transformation.setFaceModel(faceModel2);
} else { } else {
...@@ -153,17 +153,17 @@ public class ProcrustesAnalysis { ...@@ -153,17 +153,17 @@ public class ProcrustesAnalysis {
} }
return transformation; return transformation;
} }
private Vector3d getPointDistance(Point3d point1, Point3d point2) { private Vector3d getPointDistance(Point3d point1, Point3d point2) {
if (point1 == null || point2 == null) { if (point1 == null || point2 == null) {
return null; return null;
} }
return new Vector3d((point2.x - point1.x), (point2.y - point1.y), (point2.z - point1.z)); return new Vector3d((point2.x - point1.x), (point2.y - point1.y), (point2.z - point1.z));
} }
/** /**
* Moves all vertices and feature points of a face by given vector value * Moves all vertices and feature points of a face by given vector value
* *
* @param faceModel face that is moved * @param faceModel face that is moved
* @param vector vector its values are used to move a face * @param vector vector its values are used to move a face
*/ */
...@@ -175,18 +175,37 @@ public class ProcrustesAnalysis { ...@@ -175,18 +175,37 @@ public class ProcrustesAnalysis {
movePoint(v, vector); movePoint(v, vector);
} }
} }
/** /**
* Moves point by given vector value * Moves face model by given vector values.
* Different vector value is used for vertices and feature points.
* This method is used to fit feature points to already superimposed position over given face1, but move vertices
* of face back to original position of face so all the vertex movement is done by procrustes visitor.
* *
* @param faceModel on which adjustment will be applied
* @param vertexVector vertex adjustment
* @param featurePointVector feature points adjustment
*/
private void moveFaceModel(ProcrustesAnalysisFaceModel faceModel, Vector3d vertexVector, Vector3d featurePointVector) {
for (FeaturePoint fp : faceModel.getFeaturePointsMap().values()) {
movePoint(fp, featurePointVector);
}
for (MeshPoint v : faceModel.getVertices()) {
movePoint(v, vertexVector);
}
}
/**
* Moves point by given vector value
*
* @param <T> * @param <T>
* @param point * @param point
* @param vector * @param vector
*/ */
private <T extends IPosition> void movePoint(T point, Vector3d vector) { private <T extends IPosition> void movePoint(T point, Vector3d vector) {
point.getPosition().x = point.getX() + vector.x; point.getPosition().x = point.getX() + vector.x;
point.getPosition().y = point.getY() + vector.y; point.getPosition().y = point.getY() + vector.y;
point.getPosition().z = point.getZ() + vector.z; point.getPosition().z = point.getZ() + vector.z;
} }
/** /**
...@@ -206,13 +225,11 @@ public class ProcrustesAnalysis { ...@@ -206,13 +225,11 @@ public class ProcrustesAnalysis {
this.faceModel2.setFeaturePointsMap( this.faceModel2.setFeaturePointsMap(
createFeaturePointMapFromMatrix( createFeaturePointMapFromMatrix(
primaryMatrix, this.faceModel2)); primaryMatrix, this.faceModel2));
rotateVertices(this.faceModel2.getVertices(), rotationMatrix);
// rotateVertices(this.faceModel2.getVertices(), rotationMatrix);
return rotationMatrix; return rotationMatrix;
} }
/** /**
* Calculate scaling ratio of how much the appropriate object corresponding * Calculate scaling ratio of how much the appropriate object corresponding
* to the second feature point list has to be scale up or shrunk. * to the second feature point list has to be scale up or shrunk.
...@@ -226,7 +243,7 @@ public class ProcrustesAnalysis { ...@@ -226,7 +243,7 @@ public class ProcrustesAnalysis {
private double calculateScalingValue() { private double calculateScalingValue() {
List<FeaturePoint> featurePointList1 = this.faceModel1.getFeaturePointValues(); List<FeaturePoint> featurePointList1 = this.faceModel1.getFeaturePointValues();
List<FeaturePoint> featurePointList2 = this.faceModel2.getFeaturePointValues(); List<FeaturePoint> featurePointList2 = this.faceModel2.getFeaturePointValues();
double[] distancesOfList1 = new double[featurePointList1.size()]; double[] distancesOfList1 = new double[featurePointList1.size()];
double[] distancesOfList2 = new double[featurePointList2.size()]; double[] distancesOfList2 = new double[featurePointList2.size()];
...@@ -253,7 +270,8 @@ public class ProcrustesAnalysis { ...@@ -253,7 +270,8 @@ public class ProcrustesAnalysis {
} }
/** /**
* Initiates scaling of feature points and vertices of given face model by scaleFactor. * Initiates scaling of feature points and vertices of given face model by
* scaleFactor.
* *
* @param faceModel * @param faceModel
* @param scaleFactor * @param scaleFactor
...@@ -264,7 +282,8 @@ public class ProcrustesAnalysis { ...@@ -264,7 +282,8 @@ public class ProcrustesAnalysis {
} }
/** /**
* Scales each given point from list by multiplying its position coordinates with scaleFactor. * Scales each given point from list by multiplying its position coordinates
* with scaleFactor.
* *
* @param list * @param list
* @param scaleFactor * @param scaleFactor
...@@ -276,8 +295,7 @@ public class ProcrustesAnalysis { ...@@ -276,8 +295,7 @@ public class ProcrustesAnalysis {
scaledList.add(scalePointDistance(point, scaleFactor)); scaledList.add(scalePointDistance(point, scaleFactor));
} }
} }
/** /**
* Rotates all vertices. * Rotates all vertices.
* <p> * <p>
...@@ -318,7 +336,8 @@ public class ProcrustesAnalysis { ...@@ -318,7 +336,8 @@ public class ProcrustesAnalysis {
} }
/** /**
* Scales position of given point by multiplying its coordinates with given scaleFactor. * Scales position of given point by multiplying its coordinates with given
* scaleFactor.
* *
* @param point * @param point
* @param scaleFactor * @param scaleFactor
...@@ -336,11 +355,11 @@ public class ProcrustesAnalysis { ...@@ -336,11 +355,11 @@ public class ProcrustesAnalysis {
* Checks if two feature point lists have the same types of feature points. * Checks if two feature point lists have the same types of feature points.
* <p> * <p>
* To use this method you need to supply ordered lists by Feature Point type * To use this method you need to supply ordered lists by Feature Point type
* as parameters. Otherwise even if two lists contain the same feature points * as parameters. Otherwise even if two lists contain the same feature
* it will return false. * points it will return false.
* <p> * <p>
* Use sort function sortListByFeaturePointType on feature point lists * Use sort function sortListByFeaturePointType on feature point lists to
* to get correct results. * get correct results.
* *
* @param featurePointList1 * @param featurePointList1
* @param featurePointList2 * @param featurePointList2
...@@ -377,7 +396,8 @@ public class ProcrustesAnalysis { ...@@ -377,7 +396,8 @@ public class ProcrustesAnalysis {
} }
/** /**
* Creates feature point map HashMap with key FeaturePoint.type and value FeaturePoint back from matrix. * Creates feature point map HashMap with key FeaturePoint.type and value
* FeaturePoint back from matrix.
* *
* @param matrix * @param matrix
* @param model * @param model
...@@ -406,7 +426,6 @@ public class ProcrustesAnalysis { ...@@ -406,7 +426,6 @@ public class ProcrustesAnalysis {
* @param list * @param list
* @return matrix * @return matrix
*/ */
private <T extends IPosition> SimpleMatrix createMatrixFromList(List<T> list) { private <T extends IPosition> SimpleMatrix createMatrixFromList(List<T> list) {
SimpleMatrix matrix = new SimpleMatrix(list.size(), 3); SimpleMatrix matrix = new SimpleMatrix(list.size(), 3);
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
...@@ -416,6 +435,7 @@ public class ProcrustesAnalysis { ...@@ -416,6 +435,7 @@ public class ProcrustesAnalysis {
} }
return matrix; return matrix;
} }
/** /**
* Calculates distance of one feature point from another * Calculates distance of one feature point from another
* *
...@@ -426,12 +446,13 @@ public class ProcrustesAnalysis { ...@@ -426,12 +446,13 @@ public class ProcrustesAnalysis {
private double calculateDistanceFromPoint(FeaturePoint fp1, Point3d point) { private double calculateDistanceFromPoint(FeaturePoint fp1, Point3d point) {
return Math.sqrt( return Math.sqrt(
(Math.pow(fp1.getX() - point.x, 2)) (Math.pow(fp1.getX() - point.x, 2))
+ (Math.pow(fp1.getY()- point.y, 2)) + (Math.pow(fp1.getY() - point.y, 2))
+ (Math.pow(fp1.getZ()- point.z, 2))); + (Math.pow(fp1.getZ() - point.z, 2)));
} }
/** /**
* moves faces so the feature points centroids of both faces will be at the origin (0,0,0) * moves faces so the feature points centroids of both faces will be at the
* origin (0,0,0)
*/ */
protected void superImpose() { protected void superImpose() {
moveFaceModel(this.faceModel2, new Vector3d(-this.modelCentroid2.x, -this.modelCentroid2.y, -this.modelCentroid2.z)); moveFaceModel(this.faceModel2, new Vector3d(-this.modelCentroid2.x, -this.modelCentroid2.y, -this.modelCentroid2.z));
...@@ -440,7 +461,7 @@ public class ProcrustesAnalysis { ...@@ -440,7 +461,7 @@ public class ProcrustesAnalysis {
private HashMap<Integer, FeaturePoint> moveFeaturePoints(ProcrustesAnalysisFaceModel faceModel2, Vector3d pointDistance) { private HashMap<Integer, FeaturePoint> moveFeaturePoints(ProcrustesAnalysisFaceModel faceModel2, Vector3d pointDistance) {
HashMap<Integer, FeaturePoint> map = new HashMap<>(); HashMap<Integer, FeaturePoint> map = new HashMap<>();
for (Map.Entry<Integer,FeaturePoint> entry: faceModel2.getFeaturePointsMap().entrySet()) { for (Map.Entry<Integer, FeaturePoint> entry : faceModel2.getFeaturePointsMap().entrySet()) {
entry.getValue().getPosition().x += pointDistance.x; entry.getValue().getPosition().x += pointDistance.x;
entry.getValue().getPosition().y += pointDistance.y; entry.getValue().getPosition().y += pointDistance.y;
entry.getValue().getPosition().z += pointDistance.z; entry.getValue().getPosition().z += pointDistance.z;
...@@ -449,28 +470,22 @@ public class ProcrustesAnalysis { ...@@ -449,28 +470,22 @@ public class ProcrustesAnalysis {
return map; return map;
} }
private void readjustFace(ProcrustesAnalysisFaceModel faceModel, Vector3d vertexAdjustment, Vector3d fpAdjustment) {
faceModel2.setFeaturePointsMap(moveFeaturePoints(faceModel, fpAdjustment));
for (MeshPoint v : faceModel.getVertices()) {
movePoint(v, vertexAdjustment);
}
}
/** /**
* Finds corresponding subset of feature points from both faces so the procrustes analysis can be * Finds corresponding subset of feature points from both faces so the
* applied on the subset. * procrustes analysis can be applied on the subset.
* *
* @param featurePoints * @param featurePoints
* @param featurePoints0 * @param featurePoints0
* *
* @return List of feature point types that are in both sets of feature points * @return List of feature point types that are in both sets of feature
* points
*/ */
private ArrayList<Integer> getSubsetOfFeaturePoints(List<FeaturePoint> featurePoints1, List<FeaturePoint> featurePoints2) { private ArrayList<Integer> getSubsetOfFeaturePoints(List<FeaturePoint> featurePoints1, List<FeaturePoint> featurePoints2) {
HashMap<Integer, FeaturePoint> fpMap = new HashMap<>(); HashMap<Integer, FeaturePoint> fpMap = new HashMap<>();
for (FeaturePoint fp : featurePoints1) { for (FeaturePoint fp : featurePoints1) {
fpMap.put(fp.getFeaturePointType().getType(), fp); fpMap.put(fp.getFeaturePointType().getType(), fp);
} }
ArrayList<Integer> vaiablePoints = new ArrayList<>(); ArrayList<Integer> vaiablePoints = new ArrayList<>();
featurePoints2.forEach(fp -> { featurePoints2.forEach(fp -> {
if (fpMap.get(fp.getFeaturePointType().getType()) != null) { if (fpMap.get(fp.getFeaturePointType().getType()) != null) {
...@@ -486,5 +501,5 @@ public class ProcrustesAnalysis { ...@@ -486,5 +501,5 @@ public class ProcrustesAnalysis {
); );
return featurePointList; return featurePointList;
} }
} }
...@@ -68,17 +68,17 @@ public class ProcrustesVisitor extends MeshVisitor { ...@@ -68,17 +68,17 @@ public class ProcrustesVisitor extends MeshVisitor {
try { try {
ProcrustesAnalysis procrustes = new ProcrustesAnalysis(this.primaryFace, this.secondaryFace, scale); ProcrustesAnalysis procrustes = new ProcrustesAnalysis(this.primaryFace, this.secondaryFace, scale);
this.transformation = procrustes.analyze(); this.transformation = procrustes.analyze();
// applyTransformations(this.transformation, transformedFacet); applyTransformations(this.transformation, transformedFacet);
} catch (Exception e) { } catch (Exception e) {
System.err.println(e.getMessage()); System.err.println(e.getMessage());
} }
} }
private void applyTransformations(ProcrustesTransformation transformation, MeshFacet transformedFacet){ private void applyTransformations(ProcrustesTransformation transformation, MeshFacet transformedFacet){
moveFace(transformedFacet, transformation.getCentroidAdjustment()); moveFace(this.secondaryFace.getMeshModel().getFacets().get(0), transformation.getCentroidAdjustment());
scaleFace(transformedFacet, transformation.getScale()); scaleFace(this.secondaryFace.getMeshModel().getFacets().get(0), transformation.getScale());
rotateFace(transformedFacet, transformation.getRotationMatrix()); rotateFace(this.secondaryFace.getMeshModel().getFacets().get(0), transformation.getRotationMatrix());
moveFace(transformedFacet, transformation.getSuperImpositionAdjustment()); moveFace(this.secondaryFace.getMeshModel().getFacets().get(0), transformation.getSuperImpositionAdjustment());
} }
private void moveFace(MeshFacet transformedFacet, Vector3d vector) { private void moveFace(MeshFacet transformedFacet, Vector3d vector) {
......
...@@ -40,8 +40,10 @@ public class ProcrustesVisitorTest { ...@@ -40,8 +40,10 @@ public class ProcrustesVisitorTest {
private static final String FACE_2 = "0002_02_ECA.obj"; private static final String FACE_2 = "0002_02_ECA.obj";
private static final String FACE_FP_1 = "0002_01_landmarks.csv"; private static final String FACE_FP_1 = "0002_01_landmarks.csv";
private static final String FACE_FP_2 = "0002_02_landmarks.csv"; private static final String FACE_FP_2 = "0002_02_landmarks.csv";
private static final String FACE_FP_ROTATION_1 = "rotation_180_moved_2_landmarks.csv"; private static final String FACE_FP_ROTATION_1 = "rotation_landmarks.csv";
private static final String FACE_FP_ROTATION_2 = "rotation_landmarks.csv"; private static final String FACE_FP_ROTATION_2 = "rotation_180_moved_2_landmarks.csv";
private static final String FACE_FP_TWOS = "twos_landmarks.csv";
private static final String FACE_FP_FOURS = "fours_landmarks.csv";
/** /**
* Main method * Main method
...@@ -56,7 +58,7 @@ public class ProcrustesVisitorTest { ...@@ -56,7 +58,7 @@ public class ProcrustesVisitorTest {
List<FeaturePoint> fpList1 = featurePointImportService.importFeaturePoints( List<FeaturePoint> fpList1 = featurePointImportService.importFeaturePoints(
DATA_DIR.toString(), FACE_FP_ROTATION_1); DATA_DIR.toString(), FACE_FP_ROTATION_1);
List<FeaturePoint> fpList2= featurePointImportService.importFeaturePoints( List<FeaturePoint> fpList2= featurePointImportService.importFeaturePoints(
DATA_DIR.toString(), FACE_FP_2); DATA_DIR.toString(), FACE_FP_ROTATION_2);
HumanFace face1 = factory.getFace(factory.loadFace( HumanFace face1 = factory.getFace(factory.loadFace(
......
Scan name,EX_R x,EX_R y,EX_R z,EX_L x,EX_L y,EX_L z,EN_R x,EN_R y,EN_R z,EN_L x,EN_L y,EN_L z
0002_01,40,40,40,40,-40,40,-40,-40,-40,-40,40,-40
Scan name,EX_R x,EX_R y,EX_R z,EX_L x,EX_L y,EX_L z,EN_R x,EN_R y,EN_R z,EN_L x,EN_L y,EN_L z
0002_01,20,20,20,20,-20,20,-20,-20,-20,-20,20,-20
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment