Commit f1a3e70b authored by Oliver Zettining's avatar Oliver Zettining Committed by Cristina Precup
Browse files

registration demo - starter: so far, rigid registration for 3D with Mattes...

registration demo - starter: so far, rigid registration for 3D with Mattes Mutual Information metric - Cristina Precup
parent 9f09c18a
// ================================================================================================
//
// This file is part of the CAMPVis Software Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012-2013, all rights reserved,
// Christian Schulte zu Berge <christian.szb@in.tum.de>
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
//
// For a full list of authors and contributors, please refer to the file "AUTHORS.txt".
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
//
// ================================================================================================
#include "itkregistration.h"
#include "tgt/glmath.h"
#include "tgt/logmanager.h"
#include "modules/itk/core/genericimagerepresentationitk.h"
#include <itkIntTypes.h>
#include <itkCastImageFilter.h>
#include <itkImageRegistrationMethod.h>
#include <itkMattesMutualInformationImageToImageMetric.h>
#include <itkResampleImageFilter.h>
#include <itkVersorRigid3DTransform.h>
#include <itkCenteredTransformInitializer.h>
#include <itkVersorRigid3DTransformOptimizer.h>
#include <itkRescaleIntensityImageFilter.h>
#include "core/datastructures/imagedata.h"
#include "core/datastructures/genericimagerepresentationlocal.h"
// In this class we want to use various ITK registration methods.
/**
* Executes the specified registration on the data.
* \param MA_baseType base type of input images
* \param MA_returnType base type of output image
* \param MA_numChannels number of channels of input image
* \param MA_dimensionality dimensionality of images
* \param MA_registrationType type name of the ITK registration to use
* \param MA_transformationType type name of the ITK transformation to use (within itk:: namespace)
* \param MD_registrationBody additional stuff to execute between registration definition and execution
*/
#define PERFORM_ITK_REGISTRATION(MA_baseType, MA_returnType, MA_numChannels, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
{ \
GenericImageRepresentationItk<MA_baseType, MA_numChannels, MA_dimensionality>::ScopedRepresentation itkRepFixed(data, p_sourceImageIDFixed.getValue()); \
GenericImageRepresentationItk<MA_baseType, MA_numChannels, MA_dimensionality>::ScopedRepresentation itkRepMoving(data, p_sourceImageIDMoving.getValue()); \
if ((MA_dimensionality == 3) && ( itkRepFixed != 0) && (itkRepMoving != 0)) { \
typedef itk::VersorRigid3DTransformOptimizer OptimizerType; \
typedef GenericImageRepresentationItk<MA_baseType, MA_numChannels, MA_dimensionality>::ItkImageType InputImageType; \
typedef GenericImageRepresentationItk<MA_returnType, MA_numChannels, MA_dimensionality>::ItkImageType OutputImageType; \
typedef itk::LinearInterpolateImageFunction<InputImageType, double> InterpolatorType; \
typedef itk::ImageRegistrationMethod<InputImageType, InputImageType> RegistrationType; \
typedef itk::MA_registrationType<InputImageType, InputImageType> MetricType; \
typedef itk::ResampleImageFilter<InputImageType, InputImageType> ResampleFilterType; \
typedef itk::MA_transformationType<double> TransformType; \
TransformType::Pointer transform = TransformType::New(); \
OptimizerType::Pointer optimizer = OptimizerType::New(); \
InterpolatorType::Pointer interpolator = InterpolatorType::New(); \
RegistrationType::Pointer registration = RegistrationType::New(); \
MetricType::Pointer metric = MetricType::New(); \
\
registration->SetOptimizer(optimizer); \
registration->SetTransform(transform); \
registration->SetInterpolator(interpolator); \
registration->SetMetric(metric); \
MD_registrationBody \
registration->SetFixedImage(itkRepFixed->getItkImage()); \
registration->SetMovingImage(itkRepMoving->getItkImage()); \
registration->SetFixedImageRegion(itkRepFixed->getItkImage()->GetBufferedRegion()); \
typedef itk::CenteredTransformInitializer<TransformType, InputImageType, InputImageType> TransformInitializerType; \
TransformInitializerType::Pointer initializer = TransformInitializerType::New(); \
initializer->SetTransform(transform); \
initializer->SetFixedImage(itkRepFixed->getItkImage()); \
initializer->SetMovingImage(itkRepMoving->getItkImage()); \
initializer->MomentsOn(); \
initializer->InitializeTransform(); \
typedef TransformType::VersorType VersorType; \
typedef VersorType::VectorType VectorType; \
VersorType rotation; \
VectorType axis; \
axis[0] = 0.0; \
axis[1] = 0.0; \
axis[2] = 1.0; \
const double angle = 0; \
rotation.Set(axis, angle); \
transform->SetRotation(rotation); \
registration->SetInitialTransformParameters(transform->GetParameters()); \
\
typedef OptimizerType::ScalesType OptimizerScalesType; \
OptimizerScalesType optimizerScales(transform->GetNumberOfParameters()); \
optimizer->MinimizeOn(); \
const double translationScale = 1.0 / 1000.0; \
optimizerScales[0] = 1.0; \
optimizerScales[1] = 1.0; \
optimizerScales[2] = 1.0; \
optimizerScales[3] = translationScale; \
optimizerScales[4] = translationScale; \
optimizerScales[5] = translationScale; \
optimizer->SetScales(optimizerScales); \
optimizer->SetMaximumStepLength(0.2000); \
optimizer->SetMinimumStepLength(0.0001); \
optimizer->SetNumberOfIterations(200); \
registration->Update(); \
\
OptimizerType::ParametersType finalParameters = registration->GetLastTransformParameters(); \
transform->SetParameters(finalParameters); \
TransformType::Pointer finalTransform = TransformType::New(); \
finalTransform->SetCenter(transform->GetCenter()); \
finalTransform->SetParameters(finalParameters); \
finalTransform->SetFixedParameters(transform->GetFixedParameters()); \
\
ResampleFilterType::Pointer resample = ResampleFilterType::New(); \
resample->SetTransform(finalTransform); \
resample->SetInput(itkRepMoving->getItkImage()); \
MA_baseType defaultPixelValue = 100; \
resample->SetSize(itkRepFixed->getItkImage()->GetLargestPossibleRegion().GetSize()); \
resample->SetOutputOrigin(itkRepFixed->getItkImage()->GetOrigin()); \
resample->SetOutputSpacing(itkRepFixed->getItkImage()->GetSpacing()); \
resample->SetOutputDirection(itkRepFixed->getItkImage()->GetDirection()); \
resample->SetDefaultPixelValue(defaultPixelValue); \
\
itk::CastImageFilter<InputImageType, OutputImageType>::Pointer caster = itk::CastImageFilter<InputImageType, OutputImageType>::New(); \
caster->SetInput(resample->GetOutput()); \
caster->Update(); \
\
GenericImageRepresentationItk<MA_baseType, MA_numChannels, MA_dimensionality>::create(id, caster->GetOutput()); \
} \
}
#define DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, MA_baseType, MA_returnType, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
tgtAssert(MA_WTPF._numChannels == 1, "ItkRegistration only supports single-channel images.") \
PERFORM_ITK_REGISTRATION(MA_baseType, MA_returnType, 1, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody)
#define DISPATCH_ITK_REGISTRATION_D(MA_WTPF, MA_WTPM, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
switch (MA_WTPF._baseType) { \
case WeaklyTypedPointer::UINT8: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, uint8_t, uint8_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::INT8: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, int8_t, int8_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::UINT16: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, uint16_t, uint16_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::INT16: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, int16_t, int16_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::UINT32: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, uint32_t, uint32_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::INT32: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, int32_t, int32_t, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
case WeaklyTypedPointer::FLOAT: \
DISPATCH_ITK_REGISTRATION_BRD(MA_WTPF, MA_WTPM, float, float, MA_dimensionality, MA_registrationType, MA_transformationType, MD_registrationBody) \
break; \
default: \
tgtAssert(false, "Should not reach this - wrong base type in WeaklyTypedPointer!"); \
} \
/**
* Dispatches the execution for the ITK registration based on transformation \a MA_transformationType
* and registration \a MA_RegistrationType for the images \a MA_localRepFixed and \a MA_localRepMoving.
* \param MA_localRepFixed local representation of the fixed image to be registered with the moving one
* \param MA_localRepMoving local representation of the moving image to be registered with the fixed one
* \param MA_registrationType type name of the ITK registration to use
* \param MA_transformationType type name of the ITK transformation to use (within itk:: namespace)
* \param MD_registrationBody additional stuff to execute between registration definition and execution
*/
#define DISPATCH_ITK_REGISTRATION(MA_localRepFixed, MA_localRepMoving, MA_registrationType, MA_transformationType, MD_registrationBody) \
do { \
WeaklyTypedPointer wtpf = MA_localRepFixed->getWeaklyTypedPointer(); \
WeaklyTypedPointer wtpm = MA_localRepMoving->getWeaklyTypedPointer(); \
switch (MA_localRepFixed->getDimensionality()) { \
case 3: DISPATCH_ITK_REGISTRATION_D(wtpf, wtpm, 3, MA_registrationType, MA_transformationType, MD_registrationBody) break; \
default: tgtAssert(false, "Unsupported dimensionality!"); break; \
} \
} while (0)
// ================================================================================================
// = Macros defined, let the party begin! =
// ================================================================================================
namespace campvis {
static const GenericOption<std::string> registrationTypes[1] = {
GenericOption<std::string>("MattesMIRigid3D", "Mattes Mutual Information Rigid 3D")
};
const std::string ItkRegistration::loggerCat_ = "CAMPVis.modules.classification.ItkRegistration";
ItkRegistration::ItkRegistration()
: AbstractProcessor()
, p_sourceImageIDFixed("InputVolumeFixed", "Fixed Input Volume ID", "volume_fixed", DataNameProperty::READ)
, p_sourceImageIDMoving("InputVolumeMoving", "Moving Input Volume ID", "volume_moving", DataNameProperty::READ)
, p_targetImageID("OutputRegistered", "Output Registered Volume ID", "registered_volume", DataNameProperty::WRITE)
, p_registrationType("RegistrationType", "Registration Type", registrationTypes, 1)
, p_noOfBins("NoOfBins", "No. of Bins", 20, 1, 256, 1)
, p_noOfSamples("NoOfSampels", "No. of Samples", 10000, 1, 20000, 1)
{
addProperty(p_sourceImageIDFixed);
addProperty(p_sourceImageIDMoving);
addProperty(p_targetImageID);
addProperty(p_registrationType, INVALID_RESULT | INVALID_PROPERTIES);
addProperty(p_noOfBins);
}
ItkRegistration::~ItkRegistration() {
}
void ItkRegistration::updateResult(DataContainer& data) {
ImageRepresentationLocal::ScopedRepresentation inputFixed(data, p_sourceImageIDFixed.getValue());
ImageRepresentationLocal::ScopedRepresentation inputMoving(data, p_sourceImageIDMoving.getValue());
if (inputFixed != 0 && inputMoving != 0) {
size_t dimInputFixed = inputFixed->getDimensionality();
size_t dimInputMoving = inputMoving->getDimensionality();
if(inputFixed->getParent()->getNumChannels() == 1 &&
inputMoving->getParent()->getNumChannels() == 1 &&
(dimInputFixed == dimInputMoving) && dimInputFixed == 3) {
const size_t dim = dimInputFixed;
ImageData* id = new ImageData(dim, inputFixed->getSize(), 1);
if (p_registrationType.getOptionValue() == "MattesMIRigid3D") {
if (dim == 3) {
#pragma GCC diagnostic ignored "-Warray-bounds"
DISPATCH_ITK_REGISTRATION(inputFixed, inputMoving, MattesMutualInformationImageToImageMetric, VersorRigid3DTransform, \
unsigned long noOfBins = p_noOfBins.getValue(); \
unsigned long noOfSamples = p_noOfSamples.getValue(); \
metric->SetNumberOfHistogramBins(noOfBins); \
metric->SetNumberOfSpatialSamples(noOfSamples); \
);
}
else {
tgtAssert(false, "Unsupported dimensionality!");
}
}
data.addData(p_targetImageID.getValue(), id);
}
else {
LDEBUG("No suitable input image found.");
}
}
else {
LDEBUG("No suitable input image found.");
}
validate(INVALID_RESULT);
}
void ItkRegistration::updateProperties(DataContainer& /*dataContainer*/) {
if (p_registrationType.getOptionValue() == "MattesMIRigid3D") {
p_noOfBins.setVisible(true);
p_noOfSamples.setVisible(true);
}
validate(AbstractProcessor::INVALID_PROPERTIES);
}
}
// ================================================================================================
//
// This file is part of the CAMPVis Software Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012-2013, all rights reserved,
// Christian Schulte zu Berge <christian.szb@in.tum.de>
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
//
// For a full list of authors and contributors, please refer to the file "AUTHORS.txt".
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
//
// ================================================================================================
#ifndef ITKREGISTRATION_H__
#define ITKREGISTRATION_H__
#include <string>
#include "core/pipeline/visualizationprocessor.h"
#include "core/properties/datanameproperty.h"
#include "core/properties/genericproperty.h"
#include "core/properties/floatingpointproperty.h"
#include "core/properties/numericproperty.h"
#include "core/properties/optionproperty.h"
#include "modules/preprocessing/tools/abstractimagefilter.h"
namespace campvis {
/**
* Performs watershed image filter on input image using ITK.
*/
class ItkRegistration : public AbstractProcessor {
public:
/**
* Constructs a new ItkRegistration Processor
**/
ItkRegistration();
/**
* Destructor
**/
virtual ~ItkRegistration();
/// \see AbstractProcessor::getName()
virtual const std::string getName() const { return "ItkRegistration"; };
/// \see AbstractProcessor::getDescription()
virtual const std::string getDescription() const { return "Performs registration between 2 input images using ITK."; };
/// \see AbstractProcessor::getAuthor()
virtual const std::string getAuthor() const { return "Cristina Precup <cristina.precup@tum.de>"; };
/// \see AbstractProcessor::getProcessorState()
virtual ProcessorState getProcessorState() const { return AbstractProcessor::TESTING; };
DataNameProperty p_sourceImageIDFixed; ///< ID for fixed input volume
DataNameProperty p_sourceImageIDMoving; ///< ID for moving input volume
DataNameProperty p_targetImageID; ///< ID for output volume
GenericOptionProperty<std::string> p_registrationType; ///< Registration type
IntProperty p_noOfBins;
IntProperty p_noOfSamples;
protected:
/// \see AbstractProcessor::updateResult
virtual void updateResult(DataContainer& dataContainer);
/// \see AbstractProcessor::updateProperties
virtual void updateProperties(DataContainer& dataContainer);
static const std::string loggerCat_;
};
}
#endif // ITKREGISTRATION_H__
// ================================================================================================
//
// This file is part of the CAMPVis Software Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012, all rights reserved,
// Christian Schulte zu Berge <christian.szb@in.tum.de>
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
// For a full list of authors and contributors, please refer to the file "AUTHORS.txt".
//
// The licensing of this softare is not yet resolved. Until then, redistribution in source or
// binary forms outside the CAMP chair is not permitted, unless explicitly stated in legal form.
// However, the names of the original authors and the above copyright notice must retain in its
// original state in any case.
//
// Legal disclaimer provided by the BSD license:
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// ================================================================================================
#include "registrationdemo.h"
#include "tgt/event/keyevent.h"
#include "core/datastructures/imagedata.h"
#include "core/classification/geometry1dtransferfunction.h"
#include "core/classification/tfgeometry1d.h"
namespace campvis {
RegistrationDemo::RegistrationDemo(DataContainer* dc)
: AutoEvaluationPipeline(dc)
, _lsp()
, _imageReader()
, _ve(&_canvasSize)
, _itkRegistration()
{
addProcessor(&_lsp);
addProcessor(&_imageReader);
addProcessor(&_ve);
addProcessor(&_itkRegistration);
addEventListenerToBack(&_ve);
}
RegistrationDemo::~RegistrationDemo() {
}
void RegistrationDemo::init() {
AutoEvaluationPipeline::init();
_ve.p_outputImage.setValue("result");
_renderTargetID.setValue("result");
//_imageReader.setURL(CAMPVIS_SOURCE_DIR "/modules/vis/sampledata/smallHeart.mhd");
//_imageReader.setURL(CAMPVIS_SOURCE_DIR "/../misc/mha_loader_CAMPVis_volumes/prostate_phantom_US/prostate_phantom_fcal_volume_uncompressed.mhd");
_imageReader.setURL(CAMPVIS_SOURCE_DIR "/../misc/mha_loader_CAMPVis_volumes/prostate_phantom_US/prostate_phantom_fcal_volume_uncompressed.mha");
_imageReader.setTargetImageId("reader.output");
_imageReader.setTargetImageIdSharedProperty(&_ve.p_inputVolume);
Geometry1DTransferFunction* dvrTF = new Geometry1DTransferFunction(128, tgt::vec2(0.f, .05f));
dvrTF->addGeometry(TFGeometry1D::createQuad(tgt::vec2(.4f, .5f), tgt::col4(32, 192, 0, 128), tgt::col4(32, 192, 0, 128)));
dvrTF->addGeometry(TFGeometry1D::createQuad(tgt::vec2(.12f, .15f), tgt::col4(85, 0, 0, 128), tgt::col4(255, 0, 0, 128)));
dvrTF->addGeometry(TFGeometry1D::createQuad(tgt::vec2(.19f, .28f), tgt::col4(89, 89, 89, 155), tgt::col4(89, 89, 89, 155)));
dvrTF->addGeometry(TFGeometry1D::createQuad(tgt::vec2(.41f, .51f), tgt::col4(170, 170, 128, 64), tgt::col4(192, 192, 128, 64)));
static_cast<TransferFunctionProperty*>(_ve.getNestedProperty("VolumeRendererProperties::RaycasterProps::TransferFunction"))->replaceTF(dvrTF);
static_cast<FloatProperty*>(_ve.getNestedProperty("VolumeRendererProperties::RaycasterProps::SamplingRate"))->setValue(4.f);
}
void RegistrationDemo::deinit() {
_canvasSize.s_changed.disconnect(this);
AutoEvaluationPipeline::deinit();
}
}
\ No newline at end of file
// ================================================================================================
//
// This file is part of the CAMPVis Software Framework.
//
// If not explicitly stated otherwise: Copyright (C) 2012-2013, all rights reserved,
// Christian Schulte zu Berge <christian.szb@in.tum.de>
// Chair for Computer Aided Medical Procedures
// Technische Universitt Mnchen
// Boltzmannstr. 3, 85748 Garching b. Mnchen, Germany
//
// For a full list of authors and contributors, please refer to the file "AUTHORS.txt".
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
// except in compliance with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software distributed under the
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing permissions
// and limitations under the License.
//
// ================================================================================================
#ifndef RegistrationDemo_H__
#define RegistrationDemo_H__
#include "core/pipeline/autoevaluationpipeline.h"
#include "modules/base/processors/lightsourceprovider.h"
#include "modules/io/processors/genericimagereader.h"
#include "modules/vis/processors/volumeexplorer.h"
#include "modules/itk/processors/itkimagefilter.h"
#include "modules/itk/processors/itkregistration.h"
namespace campvis {
class RegistrationDemo : public AutoEvaluationPipeline {
public:
/**
* Creates a AutoEvaluationPipeline.
*/
RegistrationDemo(DataContainer* dc);
/**
* Virtual Destructor
**/
virtual ~RegistrationDemo();
/// \see AutoEvaluationPipeline::init()
virtual void init();
/// \see AutoEvaluationPipeline::deinit()
virtual void deinit();
/// \see AbstractPipeline::getName()
virtual const std::string getName() const { return getId(); };
/// \see AbstractPipeline::getId()
static const std::string getId() { return "RegistrationDemo"; };
protected:
LightSourceProvider _lsp;
GenericImageReader _imageReader;
VolumeExplorer _ve;
ItkRegistration _itkRegistration;
};
}
#endif // RegistrationDemo_H__
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment