提交 c652ab8b 编写于 作者: G gineshidalgo99

Keypoints functions same format, better handDetector tracking

上级 20d85fce
......@@ -62,6 +62,8 @@ namespace op
return width * height;
}
void recenter(const T newWidth, const T newHeight);
/**
* It returns a string with the whole Rectangle<T> data. Useful for debugging.
* The format is: `[x, y, width, height]`
......@@ -78,6 +80,10 @@ namespace op
Rectangle<T> operator/(const T value) const;
};
// Static methods
template<typename T>
Rectangle<T> recenter(const Rectangle<T>& rectangle, const T newWidth, const T newHeight);
}
#endif // OPENPOSE_CORE_RECTANGLE_HPP
......@@ -23,7 +23,7 @@ namespace op
std::vector<std::array<Rectangle<float>, 2>> trackHands(const Array<float>& poseKeypoints, const float scaleInputToOutput);
void updateTracker(const Array<float>& poseKeypoints, const std::array<Array<float>, 2>& handKeypoints);
void updateTracker(const std::array<Array<float>, 2>& handKeypoints, const unsigned long long id);
private:
enum class PosePart : unsigned int
......
......@@ -59,7 +59,7 @@ namespace op
const auto profilerKey = Profiler::timerInit(__LINE__, __FUNCTION__, __FILE__);
// Detect people hand
for (auto& tDatum : *tDatums)
spHandDetector->updateTracker(tDatum.poseKeypoints, tDatum.handKeypoints);
spHandDetector->updateTracker(tDatum.handKeypoints, tDatum.id);
// Profiling speed
Profiler::timerEnd(profilerKey);
Profiler::printAveragedTimeMsOnIterationX(profilerKey, __LINE__, __FUNCTION__, __FILE__, Profiler::DEFAULT_X);
......
......@@ -7,21 +7,27 @@
namespace op
{
float getDistance(const float* keypointPtr, const int elementA, const int elementB);
float getDistance(const Array<float>& keypoints, const int person, const int elementA, const int elementB);
void averageKeypoints(Array<float>& keypointsA, const Array<float>& keypointsB, const int personA);
void scaleKeypoints(Array<float>& keypoints, const float scale);
void scaleKeypoints(Array<float>& keypoints, const float scaleX, const float scaleY);
void scaleKeypoints(Array<float>& keypoints, const float scaleX, const float scaleY, const float offsetX, const float offsetY);
void scaleKeypoints(Array<float>& keypoints, const float scaleX, const float scaleY, const float offsetX,
const float offsetY);
void renderKeypointsCpu(Array<float>& frameArray, const Array<float>& keypoints, const std::vector<unsigned int>& pairs,
const std::vector<float> colors, const float thicknessCircleRatio, const float thicknessLineRatioWRTCircle,
void renderKeypointsCpu(Array<float>& frameArray, const Array<float>& keypoints,
const std::vector<unsigned int>& pairs, const std::vector<float> colors,
const float thicknessCircleRatio, const float thicknessLineRatioWRTCircle,
const float threshold);
Rectangle<float> getKeypointsRectangle(const float* keypointPtr, const int numberKeypoints, const float threshold);
Rectangle<float> getKeypointsRectangle(const Array<float>& keypoints, const int person, const int numberKeypoints, const float threshold);
float getAverageScore(const Array<float>& keypoints, const int person);
float getKeypointsArea(const float* keypointPtr, const int numberKeypoints, const float threshold);
float getKeypointsArea(const Array<float>& keypoints, const int person, const int numberKeypoints, const float threshold);
int getBiggestPerson(const Array<float>& keypoints, const float threshold);
}
......
......@@ -239,7 +239,7 @@ namespace op
* Note that mThreadId must be re-initialized to 0 before starting a new Wrapper configuration.
* @return unsigned int with the next thread id value.
*/
unsigned int threadIdPP();
unsigned long long threadIdPP();
/**
* TWorker concatenator (private internal function).
......@@ -1151,7 +1151,7 @@ namespace op
}
template<typename TDatums, typename TWorker, typename TQueue>
unsigned int Wrapper<TDatums, TWorker, TQueue>::threadIdPP()
unsigned long long Wrapper<TDatums, TWorker, TQueue>::threadIdPP()
{
try
{
......@@ -1162,6 +1162,7 @@ namespace op
catch (const std::exception& e)
{
error(e.what(), __LINE__, __FUNCTION__, __FILE__);
return 0ull;
}
}
......
// #include <thrust/extrema.h>
#include <openpose/utilities/errorAndLog.hpp>
#include <openpose/utilities/macros.hpp>
#include <openpose/core/maximumBase.hpp>
......@@ -15,6 +16,45 @@ namespace op
UNUSED(targetSize);
UNUSED(sourceSize);
error("CPU version not completely implemented.", __LINE__, __FUNCTION__, __FILE__);
// // TODO: ideally done, try, debug & compare to *.cu
// TODO: (maybe): remove thrust dependencies for computers without CUDA?
// const auto height = sourceSize[2];
// const auto width = sourceSize[3];
// const auto imageOffset = height * width;
// const auto num = targetSize[0];
// const auto channels = targetSize[1];
// const auto numberParts = targetSize[2];
// const auto numberSubparts = targetSize[3];
// // log("sourceSize[0]: " + std::to_string(sourceSize[0])); // = 1
// // log("sourceSize[1]: " + std::to_string(sourceSize[1])); // = #body parts + bck = 22 (hands) or 71 (face)
// // log("sourceSize[2]: " + std::to_string(sourceSize[2])); // = 368 = height
// // log("sourceSize[3]: " + std::to_string(sourceSize[3])); // = 368 = width
// // log("targetSize[0]: " + std::to_string(targetSize[0])); // = 1
// // log("targetSize[1]: " + std::to_string(targetSize[1])); // = 1
// // log("targetSize[2]: " + std::to_string(targetSize[2])); // = 21(hands) or 70 (face)
// // log("targetSize[3]: " + std::to_string(targetSize[3])); // = 3 = [x, y, score]
// // log(" ");
// for (auto n = 0; n < num; n++)
// {
// for (auto c = 0; c < channels; c++)
// {
// // // Parameters
// const auto offsetChannel = (n * channels + c);
// for (auto part = 0; part < numberParts; part++)
// {
// auto* targetPtrOffsetted = targetPtr + (offsetChannel + part) * numberSubparts;
// const auto* const sourcePtrOffsetted = sourcePtr + (offsetChannel + part) * imageOffset;
// // Option a - 6.3 fps
// const auto sourceIndexIterator = thrust::max_element(thrust::host, sourcePtrOffsetted, sourcePtrOffsetted + imageOffset);
// const auto sourceIndex = (int)(sourceIndexIterator - sourcePtrOffsetted);
// targetPtrOffsetted[0] = sourceIndex % width;
// targetPtrOffsetted[1] = sourceIndex / width;
// targetPtrOffsetted[2] = sourcePtrOffsetted[sourceIndex];
// }
// }
// }
}
catch (const std::exception& e)
{
......
......@@ -20,7 +20,7 @@ namespace op
// {
// const auto sourceThrustPtr = thrust::device_pointer_cast(sourcePtrOffsetted);
// const auto sourceIndexIterator = thrust::max_element(thrust::device, sourceThrustPtr, sourceThrustPtr + imageOffset);
// const auto sourceIndex = sourceIndexIterator - sourceThrustPtr;
// const auto sourceIndex = (int)(sourceIndexIterator - sourceThrustPtr);
// targetPtrOffsetted[0] = sourceIndex % width;
// targetPtrOffsetted[1] = sourceIndex / width;
// targetPtrOffsetted[2] = sourcePtrOffsetted[sourceIndex];
......@@ -40,7 +40,7 @@ namespace op
// const auto* const sourcePtrOffsetted = sourcePtr + (offsetChannel + part) * imageOffset;
// auto sourceThrustPtr = thrust::device_pointer_cast(sourcePtrOffsetted);
// const auto sourceIndexIterator = thrust::max_element(thrust::device, sourceThrustPtr, sourceThrustPtr + imageOffset);
// const auto sourceIndex = sourceIndexIterator - sourceThrustPtr;
// const auto sourceIndex = (int)(sourceIndexIterator - sourceThrustPtr);
// targetPtrOffsetted[0] = sourceIndex % width;
// targetPtrOffsetted[1] = sourceIndex / width;
// targetPtrOffsetted[2] = sourcePtrOffsetted[sourceIndex];
......@@ -82,7 +82,7 @@ namespace op
// Option a - 6.3 fps
const auto sourceThrustPtr = thrust::device_pointer_cast(sourcePtrOffsetted);
const auto sourceIndexIterator = thrust::max_element(thrust::device, sourceThrustPtr, sourceThrustPtr + imageOffset);
const auto sourceIndex = sourceIndexIterator - sourceThrustPtr;
const auto sourceIndex = (int)(sourceIndexIterator - sourceThrustPtr);
fillTargetPtrPart<<<1, 1>>>(targetPtrOffsetted, sourcePtrOffsetted, sourceIndex, sourceIndex % width, sourceIndex / width);
// // Option b - <1 fps
// fillTargetPtrChannel<<<1, 1>>>(targetPtrOffsetted, sourcePtrOffsetted, width, imageOffset);
......
......@@ -111,6 +111,23 @@ namespace op
}
}
template<typename T>
void Rectangle<T>::recenter(const T newWidth, const T newHeight)
{
try
{
const auto centerPoint = center();
x = centerPoint.x - T(newWidth / 2.f);
y = centerPoint.y - T(newHeight / 2.f);
width = newWidth;
height = newHeight;
}
catch (const std::exception& e)
{
error(e.what(), __LINE__, __FUNCTION__, __FILE__);
}
}
template<typename T>
std::string Rectangle<T>::toString() const
{
......@@ -192,4 +209,42 @@ namespace op
}
COMPILE_TEMPLATE_BASIC_TYPES_STRUCT(Rectangle);
// Static methods
template<typename T>
Rectangle<T> recenter(const Rectangle<T>& rectangle, const T newWidth, const T newHeight)
{
try
{
Rectangle<T> result;
const auto centerPoint = rectangle.center();
result.x = centerPoint.x - T(newWidth / 2.f);
result.y = centerPoint.y - T(newHeight / 2.f);
result.width = newWidth;
result.height = newHeight;
return result;
}
catch (const std::exception& e)
{
error(e.what(), __LINE__, __FUNCTION__, __FILE__);
return Rectangle<T>{};
}
}
template Rectangle<char> recenter(const Rectangle<char>& rectangle, const char newWidth, const char newHeight);
template Rectangle<signed char> recenter(const Rectangle<signed char>& rectangle, const signed char newWidth, const signed char newHeight);
template Rectangle<short> recenter(const Rectangle<short>& rectangle, const short newWidth, const short newHeight);
template Rectangle<int> recenter(const Rectangle<int>& rectangle, const int newWidth, const int newHeight);
template Rectangle<long> recenter(const Rectangle<long>& rectangle, const long newWidth, const long newHeight);
template Rectangle<long long> recenter(const Rectangle<long long>& rectangle, const long long newWidth, const long long newHeight);
template Rectangle<unsigned char> recenter(const Rectangle<unsigned char>& rectangle, const unsigned char newWidth, const unsigned char newHeight);
template Rectangle<unsigned short> recenter(const Rectangle<unsigned short>& rectangle, const unsigned short newWidth, const unsigned short newHeight);
template Rectangle<unsigned int> recenter(const Rectangle<unsigned int>& rectangle, const unsigned int newWidth, const unsigned int newHeight);
template Rectangle<unsigned long> recenter(const Rectangle<unsigned long>& rectangle, const unsigned long newWidth, const unsigned long newHeight);
template Rectangle<unsigned long long> recenter(const Rectangle<unsigned long long>& rectangle, const unsigned long long newWidth, const unsigned long long newHeight);
template Rectangle<float> recenter(const Rectangle<float>& rectangle, const float newWidth, const float newHeight);
template Rectangle<double> recenter(const Rectangle<double>& rectangle, const double newWidth, const double newHeight);
template Rectangle<long double> recenter(const Rectangle<long double>& rectangle, const long double newWidth, const long double newHeight);
}
......@@ -41,7 +41,7 @@ namespace op
{
pointTopLeft.x = posePtr[headNose*3];
pointTopLeft.y = posePtr[headNose*3+1];
faceSize = 1.33f * getDistance(posePtr, neck, headNose);
faceSize = 1.33f * getDistance(poseKeypoints, personIndex, neck, headNose);
}
}
// Face as average between different body keypoints (e.g. COCO)
......@@ -59,13 +59,13 @@ namespace op
{
pointTopLeft.x += (posePtr[lEye*3] + posePtr[lEar*3] + posePtr[headNose*3]) / 3.f;
pointTopLeft.y += (posePtr[lEye*3+1] + posePtr[lEar*3+1] + posePtr[headNose*3+1]) / 3.f;
faceSize += 0.85f * (getDistance(posePtr, headNose, lEye) + getDistance(posePtr, headNose, lEar) + getDistance(posePtr, neck, headNose));
faceSize += 0.85f * (getDistance(poseKeypoints, personIndex, headNose, lEye) + getDistance(poseKeypoints, personIndex, headNose, lEar) + getDistance(poseKeypoints, personIndex, neck, headNose));
}
else // if(lEyeScoreAbove)
{
pointTopLeft.x += (posePtr[rEye*3] + posePtr[rEar*3] + posePtr[headNose*3]) / 3.f;
pointTopLeft.y += (posePtr[rEye*3+1] + posePtr[rEar*3+1] + posePtr[headNose*3+1]) / 3.f;
faceSize += 0.85f * (getDistance(posePtr, headNose, rEye) + getDistance(posePtr, headNose, rEar) + getDistance(posePtr, neck, headNose));
faceSize += 0.85f * (getDistance(poseKeypoints, personIndex, headNose, rEye) + getDistance(poseKeypoints, personIndex, headNose, rEar) + getDistance(poseKeypoints, personIndex, neck, headNose));
}
}
// else --> 2 * dist(neck, headNose)
......@@ -73,7 +73,7 @@ namespace op
{
pointTopLeft.x += (posePtr[neck*3] + posePtr[headNose*3]) / 2.f;
pointTopLeft.y += (posePtr[neck*3+1] + posePtr[headNose*3+1]) / 2.f;
faceSize += 2.f * getDistance(posePtr, neck, headNose);
faceSize += 2.f * getDistance(poseKeypoints, personIndex, neck, headNose);
}
counter++;
}
......@@ -82,7 +82,7 @@ namespace op
{
pointTopLeft.x += (posePtr[lEye*3] + posePtr[rEye*3]) / 2.f;
pointTopLeft.y += (posePtr[lEye*3+1] + posePtr[rEye*3+1]) / 2.f;
faceSize += 3.f * getDistance(posePtr, lEye, rEye);
faceSize += 3.f * getDistance(poseKeypoints, personIndex, lEye, rEye);
counter++;
}
// 2 * dist(lEar, rEar)
......@@ -90,7 +90,7 @@ namespace op
{
pointTopLeft.x += (posePtr[lEar*3] + posePtr[rEar*3]) / 2.f;
pointTopLeft.y += (posePtr[lEar*3+1] + posePtr[rEar*3+1]) / 2.f;
faceSize += 2.f * getDistance(posePtr, lEar, rEar);
faceSize += 2.f * getDistance(poseKeypoints, personIndex, lEar, rEar);
counter++;
}
// Average (if counter > 0)
......
......@@ -25,8 +25,8 @@ namespace op
// pos_hand = pos_wrist + ratio * (pos_wrist - pos_elbox) = (1 + ratio) * pos_wrist - ratio * pos_elbox
handRectangle.x = posePtr[wrist*3] + ratioWristElbow * (posePtr[wrist*3] - posePtr[elbow*3]);
handRectangle.y = posePtr[wrist*3+1] + ratioWristElbow * (posePtr[wrist*3+1] - posePtr[elbow*3+1]);
const auto distanceWristElbow = getDistance(posePtr, wrist, elbow);
const auto distanceElbowShoulder = getDistance(posePtr, elbow, shoulder);
const auto distanceWristElbow = getDistance(poseKeypoints, person, wrist, elbow);
const auto distanceElbowShoulder = getDistance(poseKeypoints, person, elbow, shoulder);
handRectangle.width = 1.5f * fastMax(distanceWristElbow, 0.9f * distanceElbowShoulder);
}
// height = width
......@@ -188,37 +188,40 @@ namespace op
}
}
void HandDetector::updateTracker(const Array<float>& poseKeypoints, const std::array<Array<float>, 2>& handKeypoints)
void HandDetector::updateTracker(const std::array<Array<float>, 2>& handKeypoints, const unsigned long long id)
{
try
{
std::lock_guard<std::mutex> lock{mMutex};
// Security checks
if (poseKeypoints.getSize(0) != handKeypoints[0].getSize(0) || poseKeypoints.getSize(0) != handKeypoints[1].getSize(0))
error("Number people on poseKeypoints different than in handKeypoints.", __LINE__, __FUNCTION__, __FILE__);
// Parameters
const auto numberPeople = poseKeypoints.getSize(0);
// const auto poseNumberParts = poseKeypoints.getSize(1);
const auto handNumberParts = handKeypoints[0].getSize(1);
const auto numberChannels = poseKeypoints.getSize(2);
const auto thresholdRectangle = 0.25f;
// Update pose keypoints and hand rectangles
mPoseTrack.resize(numberPeople);
mHandLeftPrevious.clear();
mHandRightPrevious.clear();
for (auto person = 0 ; person < mPoseTrack.size() ; person++)
if (mCurrentId < id)
{
const auto offset = person * handNumberParts * numberChannels;
// Left hand
const auto* handLeftPtr = handKeypoints[0].getConstPtr() + offset;
const auto handLeftRectangle = getKeypointsRectangle(handLeftPtr, handNumberParts, thresholdRectangle);
if (handLeftRectangle.area() > 0)
mHandLeftPrevious.emplace_back(handLeftRectangle);
const auto* handRightPtr = handKeypoints[1].getConstPtr() + offset;
// Right hand
const auto handRightRectangle = getKeypointsRectangle(handRightPtr, handNumberParts, thresholdRectangle);
if (handRightRectangle.area() > 0)
mHandRightPrevious.emplace_back(handRightRectangle);
mCurrentId = id;
// Parameters
const auto numberPeople = handKeypoints.at(0).getSize(0);
const auto handNumberParts = handKeypoints[0].getSize(1);
const auto thresholdRectangle = 0.25f;
// Update pose keypoints and hand rectangles
mPoseTrack.resize(numberPeople);
mHandLeftPrevious.clear();
mHandRightPrevious.clear();
for (auto person = 0 ; person < mPoseTrack.size() ; person++)
{
const auto scoreThreshold = 0.66667f;
// Left hand
if (getAverageScore(handKeypoints[0], person) > scoreThreshold)
{
const auto handLeftRectangle = getKeypointsRectangle(handKeypoints[0], person, handNumberParts, thresholdRectangle);
if (handLeftRectangle.area() > 0)
mHandLeftPrevious.emplace_back(handLeftRectangle);
}
// Right hand
if (getAverageScore(handKeypoints[1], person) > scoreThreshold)
{
const auto handRightRectangle = getKeypointsRectangle(handKeypoints[1], person, handNumberParts, thresholdRectangle);
if (handRightRectangle.area() > 0)
mHandRightPrevious.emplace_back(handRightRectangle);
}
}
}
}
catch (const std::exception& e)
......
......@@ -8,10 +8,11 @@ namespace op
{
const std::string errorMessage = "The Array<float> is not a RGB image. This function is only for array of dimension: [sizeA x sizeB x 3].";
float getDistance(const float* keypointPtr, const int elementA, const int elementB)
float getDistance(const Array<float>& keypoints, const int person, const int elementA, const int elementB)
{
try
{
const auto keypointPtr = keypoints.getConstPtr() + person * keypoints.getSize(1) * keypoints.getSize(2);
const auto pixelX = keypointPtr[elementA*3] - keypointPtr[elementB*3];
const auto pixelY = keypointPtr[elementA*3+1] - keypointPtr[elementB*3+1];
return std::sqrt(pixelX*pixelX+pixelY*pixelY);
......@@ -23,6 +24,36 @@ namespace op
}
}
void averageKeypoints(Array<float>& keypointsA, const Array<float>& keypointsB, const int personA)
{
try
{
// Security checks
if (keypointsA.getNumberDimensions() != keypointsB.getNumberDimensions())
error("keypointsA.getNumberDimensions() != keypointsB.getNumberDimensions().", __LINE__, __FUNCTION__, __FILE__);
for (auto dimension = 1 ; dimension < keypointsA.getNumberDimensions() ; dimension++)
if (keypointsA.getSize(dimension) != keypointsB.getSize(dimension))
error("keypointsA.getSize() != keypointsB.getSize().", __LINE__, __FUNCTION__, __FILE__);
// For each body part
const auto numberParts = keypointsA.getSize(1);
for (auto part = 0 ; part < numberParts ; part++)
{
const auto finalIndexA = keypointsA.getSize(2)*(personA*numberParts + part);
const auto finalIndexB = keypointsA.getSize(2)*part;
if (keypointsB[finalIndexB+2] - keypointsA[finalIndexA+2] > 0.05f)
{
keypointsA[finalIndexA] = keypointsB[finalIndexB];
keypointsA[finalIndexA+1] = keypointsB[finalIndexB+1];
keypointsA[finalIndexA+2] = keypointsB[finalIndexB+2];
}
}
}
catch (const std::exception& e)
{
error(e.what(), __LINE__, __FUNCTION__, __FILE__);
}
}
void scaleKeypoints(Array<float>& keypoints, const float scale)
{
try
......@@ -126,12 +157,11 @@ namespace op
const auto numberColors = colors.size();
const auto thresholdRectangle = 0.1f;
const auto numberKeypoints = keypoints.getSize(1);
const auto areaKeypoints = numberKeypoints * keypoints.getSize(2);
// Keypoints
for (auto person = 0 ; person < keypoints.getSize(0) ; person++)
{
const auto personRectangle = getKeypointsRectangle(&keypoints[person*areaKeypoints], numberKeypoints, thresholdRectangle);
const auto personRectangle = getKeypointsRectangle(keypoints, person, numberKeypoints, thresholdRectangle);
if (personRectangle.area() > 0)
{
const auto ratioAreas = fastMin(1.f, fastMax(personRectangle.width/(float)width, personRectangle.height/(float)height));
......@@ -187,13 +217,15 @@ namespace op
}
}
Rectangle<float> getKeypointsRectangle(const float* keypointPtr, const int numberKeypoints, const float threshold)
Rectangle<float> getKeypointsRectangle(const Array<float>& keypoints, const int person, const int numberKeypoints, const float threshold)
{
try
{
// Security checks
if (numberKeypoints < 1)
error("Number body parts must be > 0", __LINE__, __FUNCTION__, __FILE__);
// Define keypointPtr
const auto keypointPtr = keypoints.getConstPtr() + person * keypoints.getSize(1) * keypoints.getSize(2);
float minX = std::numeric_limits<float>::max();
float maxX = 0.f;
float minY = minX;
......@@ -229,11 +261,35 @@ namespace op
}
}
float getKeypointsArea(const float* keypointPtr, const int numberKeypoints, const float threshold)
float getAverageScore(const Array<float>& keypoints, const int person)
{
try
{
// Security checks
if (person >= keypoints.getSize(0))
error("Person index out of bounds.", __LINE__, __FUNCTION__, __FILE__);
// Get average score
auto score = 0.f;
const auto numberKeypoints = keypoints.getSize(1);
const auto area = numberKeypoints * keypoints.getSize(2);
const auto personOffset = person * area;
for (auto part = 0 ; part < numberKeypoints ; part++)
score += keypoints[personOffset + part*keypoints.getSize(2) + 2];
return score / numberKeypoints;
}
catch (const std::exception& e)
{
error(e.what(), __LINE__, __FUNCTION__, __FILE__);
return 0.f;
}
}
float getKeypointsArea(const Array<float>& keypoints, const int person, const int numberKeypoints,
const float threshold)
{
try
{
return getKeypointsRectangle(keypointPtr, numberKeypoints, threshold).area();
return getKeypointsRectangle(keypoints, person, numberKeypoints, threshold).area();
}
catch (const std::exception& e)
{
......@@ -250,12 +306,11 @@ namespace op
{
const auto numberPeople = keypoints.getSize(0);
const auto numberKeypoints = keypoints.getSize(1);
const auto area = numberKeypoints * keypoints.getSize(2);
auto biggestPoseIndex = -1;
auto biggestArea = -1.f;
for (auto person = 0 ; person < numberPeople ; person++)
{
const auto newPersonArea = getKeypointsArea(&keypoints[person*area], numberKeypoints, threshold);
const auto newPersonArea = getKeypointsArea(keypoints, person, numberKeypoints, threshold);
if (newPersonArea > biggestArea)
{
biggestArea = newPersonArea;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册