提交 0eeeccd9 编写于 作者: J Jerome Etienne

early support for js-aruco

上级 48bee7b7
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<br/> <br/>
Contact me any time at <a href='https://twitter.com/jerome_etienne' target='_blank'>@jerome_etienne</a> Contact me any time at <a href='https://twitter.com/jerome_etienne' target='_blank'>@jerome_etienne</a>
</div><script> </div><script>
var useAruco = true
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
// Init // Init
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
...@@ -54,7 +55,11 @@ ...@@ -54,7 +55,11 @@
////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////
// Create a camera // Create a camera
if( useAruco === true ){
var camera = new THREE.PerspectiveCamera(42, renderer.domElement.width / renderer.domElement.height, 0.01, 100);
}else{
var camera = new THREE.Camera(); var camera = new THREE.Camera();
}
scene.add(camera); scene.add(camera);
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
...@@ -89,6 +94,7 @@ ...@@ -89,6 +94,7 @@
arToolkitSource.copySizeTo(arToolkitContext.arController.canvas) arToolkitSource.copySizeTo(arToolkitContext.arController.canvas)
} }
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// initialize arToolkitContext // initialize arToolkitContext
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
...@@ -101,8 +107,10 @@ ...@@ -101,8 +107,10 @@
}) })
// initialize it // initialize it
arToolkitContext.initAruco(function onCompleted(){ arToolkitContext.initAruco(function onCompleted(){
if( useAruco === false ){
// copy projection matrix to camera // copy projection matrix to camera
camera.projectionMatrix.copy( arToolkitContext.getProjectionMatrix() ); camera.projectionMatrix.copy( arToolkitContext.getProjectionMatrix() );
}
}) })
// update artoolkit on every frame // update artoolkit on every frame
...@@ -121,9 +129,12 @@ ...@@ -121,9 +129,12 @@
// init controls for camera // init controls for camera
var markerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, { var markerControls = new THREEx.ArMarkerControls(arToolkitContext, camera, {
type : 'pattern', type: 'barcode',
patternUrl : THREEx.ArToolkitContext.baseURL + '../data/data/patt.hiro', barcodeValue: 1001,
// patternUrl : THREEx.ArToolkitContext.baseURL + '../data/data/patt.kanji',
// type : 'pattern',
// patternUrl : THREEx.ArToolkitContext.baseURL + '../data/data/patt.hiro',
// as we controls the camera, set changeMatrixMode: 'cameraTransformMatrix' // as we controls the camera, set changeMatrixMode: 'cameraTransformMatrix'
changeMatrixMode: 'cameraTransformMatrix' changeMatrixMode: 'cameraTransformMatrix'
}) })
......
...@@ -99,13 +99,13 @@ ...@@ -99,13 +99,13 @@
// create atToolkitContext // create atToolkitContext
var arToolkitContext = new THREEx.ArToolkitContext({ var arToolkitContext = new THREEx.ArToolkitContext({
cameraParametersUrl: THREEx.ArToolkitContext.baseURL + '../data/data/camera_para.dat', cameraParametersUrl: THREEx.ArToolkitContext.baseURL + '../data/data/camera_para.dat',
detectionMode: 'mono_and_matrix', // detectionMode: 'mono_and_matrix',
// detectionMode: 'mono', detectionMode: 'mono',
// detectionMode: 'color_and_matrix', // detectionMode: 'color_and_matrix',
// canvasWidth: 80*3, // canvasWidth: 80*3,
// canvasHeight: 60*3, // canvasHeight: 60*3,
matrixCodeType: '3x3', // matrixCodeType: '3x3',
// matrixCodeType: '3x3_HAMMING63', // matrixCodeType: '3x3_HAMMING63',
// matrixCodeType: '3x3_PARITY65', // matrixCodeType: '3x3_PARITY65',
// matrixCodeType: '4x4', // matrixCodeType: '4x4',
...@@ -133,11 +133,11 @@ ...@@ -133,11 +133,11 @@
var markerRoot = new THREE.Group var markerRoot = new THREE.Group
scene.add(markerRoot) scene.add(markerRoot)
var markerControls = new THREEx.ArMarkerControls(arToolkitContext, markerRoot, { var markerControls = new THREEx.ArMarkerControls(arToolkitContext, markerRoot, {
type: 'barcode', // type: 'barcode',
barcodeValue: 5, // barcodeValue: 5,
// type : 'pattern', type : 'pattern',
// patternUrl : 'marker-training/examples/pattern-files/pattern-hiro.patt', patternUrl : 'marker-training/examples/pattern-files/pattern-hiro.patt',
}) })
......
watch: build
fswatch -0 three.js/*.js | xargs -0 -n 1 -I {} make build
.PHONY: build
build:
cat vendor/js-aruco/src/aruco.js \
vendor/js-aruco/src/cv.js \
vendor/js-aruco/src/posit1.js \
vendor/js-aruco/src/svd.js \
threex-*.js > build/threex-aruco.js
minify: build
uglifyjs build/threex-aruco.js > build/threex-aruco-min.js
prepack: build
prepack build/threex-aruco.js --out build/threex-aruco-prepacked.js
Trying to use js-aruco as ar.js backend
- expose all the hardcoded parameters from AR.Detect in the debug,html
- thus you can tune them
---
- see how to include it in ar.js
- so basically a context and a controls
- the source can remain the same without trouble
- how to handle all the options between the various backend
# Performances
- jsaruco use a kernel size of 2 in adaptative thresholding
- could i reduce the resolution of the source image and use a kernel size of 1 ?
- it would produce more fps. what the difference would be ? create errors ?
- jsaruco - adaptiveThreshold is doing it on ALL bytes - so all channel ??? check if it is correct
- it use blackwhite image - it only needs 1 channel - 8 bits is already a lot to store blackwhite
- this mean 4 times more work than needed
- NOTES: unclear this is true - grayscale is packing it all in 1 channel. check it out ?
- in posit1: image difference is computed by a manathan distance. not a euclidian distance... how come ?
- imageDifference += Math.abs(sopImagePoints[i].x - oldSopImagePoints[i].x);
imageDifference += Math.abs(sopImagePoints[i].y - oldSopImagePoints[i].y);
<html>
<!-- <script src='vendor/Three.js'></script> -->
<script src='../../vendor/three.js/build/three.js'></script>
<script src='../vendor/js-aruco/src/svd.js'></script>
<script src='../vendor/js-aruco/src/posit1.js'></script>
<script src='../vendor/js-aruco/src/cv.js'></script>
<script src='../vendor/js-aruco/src/aruco.js'></script>
<script src='../threex-arucocontext.js'></script>
<script src='../threex-arucodebug.js'></script>
<!-- <script src='../build/threex-aruco-min.js'></script> -->
<!-- <script src='../build/threex-aruco-prepacked.js'></script> -->
<div>
<form>
<label>clear canvas<input id='checkboxClearCanvas' name="imgsel" type="radio"></label>
<br/>
<label>draw video<input id='checkboxDrawVideo' name="imgsel" type="radio" checked="checked" ></label>
<br/>
<label>draw detector grey<input id='checkboxDetectorGrey' name="imgsel" type="radio"></label>
<br/>
<label>draw detector threshold<input id='checkboxDetectorThreshold' name="imgsel" type="radio"></label>
</form>
<label>draw marker corners<input id='checkboxMarkerCorners' type="checkbox" checked="checked" ></label>
<br/>
<label>draw marker ids<input id='checkboxMarkerId' type="checkbox" checked="checked" ></label>
<br/>
<label>draw contours contours<input id='checkboxContoursContours' type="checkbox"></label>
<br/>
<label>draw contours polys<input id='checkboxContoursPolys' type="checkbox"></label>
<br/>
<label>draw contours candiddates<input id='checkboxContoursCandidates' type="checkbox"></label>
</div>
<body style='background-color: darkgray;'>
<div id='webGLContainer' style='display: inline;'></div>
<script>
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
var videoElement = document.createElement('video')
// videoElement.width = 320
// videoElement.height = 240
videoElement.autoplay = true
//////////////////////////////////////////////////////////////////////////////
// Code Separator
//////////////////////////////////////////////////////////////////////////////
navigator.getUserMedia({video:true}, function (stream){
if (window.URL) {
videoElement.src = window.URL.createObjectURL(stream);
} else if (videoElement.mozSrcObject !== undefined) {
videoElement.mozSrcObject = stream;
} else {
videoElement.src = stream;
}
},
function(error){
}
);
var markerSize = 1.0; //millimeters
var arucoContext = new THREEx.ArucoContext(markerSize)
var arucoDebug = new THREEx.ArucoDebug(arucoContext)
document.body.appendChild(arucoDebug.canvasElement)
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xffffff, 1);
renderer.setSize(arucoContext.canvas.width, arucoContext.canvas.height);
document.getElementById('webGLContainer').appendChild(renderer.domElement);
var sceneOrtho = new THREE.Scene();
var cameraOrtho = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5);
sceneOrtho.add(cameraOrtho);
var scenePersp = new THREE.Scene();
var cameraPersp = new THREE.PerspectiveCamera(42, arucoContext.canvas.width / arucoContext.canvas.height, 0.01, 100);
scenePersp.add(cameraPersp);
//////////////////////////////////////////////////////////////////////////////
// Code Separator
//////////////////////////////////////////////////////////////////////////////
var videoTexture = new THREE.Texture(videoElement)
videoTexture.minFilter = THREE.LinearFilter
var geometry = new THREE.PlaneGeometry(1.0, 1.0)
var material = new THREE.MeshBasicMaterial({
map: videoTexture,
depthTest: false,
depthWrite: false
})
var videoMesh = new THREE.Mesh(geometry, material);
videoMesh.position.z = -1;
sceneOrtho.add(videoMesh);
var geometry = new THREE.PlaneGeometry(1.0, 1.0, 10, 10)
var material = new THREE.MeshBasicMaterial( {
color: 'hotpink',
wireframe: true
} )
var model = new THREE.Mesh(geometry, material);
var arWorldRoot = new THREE.Group
arWorldRoot.add(model)
scenePersp.add(arWorldRoot);
//////////////////////////////////////////////////////////////////////////////
// render loop
//////////////////////////////////////////////////////////////////////////////
var lastDetectionAt = null
requestAnimationFrame(function onAnimationFrame(){
// update videoMesh
videoMesh.material.map.needsUpdate = true;
var present = Date.now()/1000
if( lastDetectionAt === null || present-lastDetectionAt >= 1/30){
lastDetectionAt = Date.now()/1000
// if( videoElement.readyState >= videoElement.HAVE_CURRENT_DATA ){
// detect markers in imageData
var detectedMarkers = arucoContext.detect(videoElement)
if( detectedMarkers.length > 0 ){
var detectedMarker = detectedMarkers[0]
THREEx.ArucoContext.updateObject3D(arWorldRoot, detectedMarker);
arWorldRoot.visible = true
}else{
arWorldRoot.visible = false
}
if( document.querySelector('#checkboxClearCanvas').checked ) arucoDebug.clear()
if( document.querySelector('#checkboxDrawVideo').checked ) arucoDebug.drawVideo(videoElement)
if( document.querySelector('#checkboxDetectorGrey').checked ) arucoDebug.drawDetectorGrey()
if( document.querySelector('#checkboxDetectorThreshold').checked ) arucoDebug.drawDetectorThreshold()
if( document.querySelector('#checkboxMarkerCorners').checked ) arucoDebug.drawMarkerCorners(detectedMarkers)
if( document.querySelector('#checkboxMarkerId').checked ) arucoDebug.drawMarkerIDs(detectedMarkers)
if( document.querySelector('#checkboxContoursContours').checked ) arucoDebug.drawContoursContours()
if( document.querySelector('#checkboxContoursPolys').checked ) arucoDebug.drawContoursPolys()
if( document.querySelector('#checkboxContoursCandidates').checked ) arucoDebug.drawContoursCandidates()
}
// render scene
renderer.autoClear = false;
renderer.clear();
renderer.render(sceneOrtho, cameraOrtho);
renderer.render(scenePersp, cameraPersp);
requestAnimationFrame(onAnimationFrame);
});
</script>
<style media="screen">
img {
width: 128px;
height: 128px;
padding: 8em;
}
</style>
<br/>
<div>
<img src="images/1001.png"/>
<img src="images/1001.png"/>
<img src="images/1001.png"/>
<img src="images/1001.png"/>
</div>
</body>
</html>
<script src="../vendor/aruco-marker.js"></script>
<script src="../threex-arucomarkergenerator.js"></script>
<body><script>
// var domElement = THREEx.ArucoMarkerGenerator.createSVG(1001, '100px')
// document.body.appendChild(domElement)
var domElement = THREEx.ArucoMarkerGenerator.createIMG(1001, '256px')
document.body.appendChild(domElement)
</script></body>
<html>
<!-- <script src='vendor/Three.js'></script> -->
<script src='../../vendor/three.js/build/three.js'></script>
<script src='../vendor/js-aruco/src/svd.js'></script>
<script src='../vendor/js-aruco/src/posit1.js'></script>
<script src='../vendor/js-aruco/src/cv.js'></script>
<script src='../vendor/js-aruco/src/aruco.js'></script>
<script src='../threex-arucocontext.js'></script>
<script src='../threex-arucodebug.js'></script>
<!-- <script src='../build/threex-aruco-min.js'></script> -->
<!-- <script src='../build/threex-aruco-prepacked.js'></script> -->
<body style='background-color: darkgray;'>
<div id='webGLContainer' style='display: inline;'></div>
<script>
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
var videoElement = document.createElement('video')
// videoElement.width = 320
// videoElement.height = 240
videoElement.autoplay = true
//////////////////////////////////////////////////////////////////////////////
// Code Separator
//////////////////////////////////////////////////////////////////////////////
navigator.getUserMedia({video:true}, function (stream){
if (window.URL) {
videoElement.src = window.URL.createObjectURL(stream);
} else if (videoElement.mozSrcObject !== undefined) {
videoElement.mozSrcObject = stream;
} else {
videoElement.src = stream;
}
},
function(error){
}
);
var markerSize = 1.0; //millimeters
var arucoContext = new THREEx.ArucoContext(markerSize)
var renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0xffffff, 1);
renderer.setSize(arucoContext.canvas.width, arucoContext.canvas.height);
document.getElementById('webGLContainer').appendChild(renderer.domElement);
var sceneOrtho = new THREE.Scene();
var cameraOrtho = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5);
sceneOrtho.add(cameraOrtho);
var scenePersp = new THREE.Scene();
var cameraPersp = new THREE.PerspectiveCamera(42, arucoContext.canvas.width / arucoContext.canvas.height, 0.01, 100);
scenePersp.add(cameraPersp);
//////////////////////////////////////////////////////////////////////////////
// Code Separator
//////////////////////////////////////////////////////////////////////////////
var videoTexture = new THREE.Texture(videoElement)
videoTexture.minFilter = THREE.LinearFilter
var geometry = new THREE.PlaneGeometry(1.0, 1.0)
var material = new THREE.MeshBasicMaterial({
map: videoTexture,
depthTest: false,
depthWrite: false
})
var videoMesh = new THREE.Mesh(geometry, material);
videoMesh.position.z = -1;
sceneOrtho.add(videoMesh);
var geometry = new THREE.PlaneGeometry(1.0, 1.0, 10, 10)
var material = new THREE.MeshBasicMaterial( {
color: 'hotpink',
wireframe: true
} )
var model = new THREE.Mesh(geometry, material);
var arWorldRoot = new THREE.Group
arWorldRoot.add(model)
scenePersp.add(arWorldRoot);
//////////////////////////////////////////////////////////////////////////////
// render loop
//////////////////////////////////////////////////////////////////////////////
var lastDetectionAt = null
requestAnimationFrame(function onAnimationFrame(){
// update videoMesh
videoMesh.material.map.needsUpdate = true;
var present = Date.now()/1000
if( lastDetectionAt === null || present-lastDetectionAt >= 1/30){
lastDetectionAt = Date.now()/1000
// if( videoElement.readyState >= videoElement.HAVE_CURRENT_DATA ){
// detect markers in imageData
// console.time('detect');
var detectedMarkers = arucoContext.detect(videoElement)
// console.timeEnd('detect');
if( detectedMarkers.length > 0 ){
var detectedMarker = detectedMarkers[0]
THREEx.ArucoContext.updateObject3D(arWorldRoot, detectedMarker);
arWorldRoot.visible = true
}else{
arWorldRoot.visible = false
}
}
// render scene
renderer.autoClear = false;
renderer.clear();
renderer.render(sceneOrtho, cameraOrtho);
renderer.render(scenePersp, cameraPersp);
requestAnimationFrame(onAnimationFrame);
});
</script>
<style media="screen">
img {
width: 128px;
height: 128px;
padding: 8em;
}
</style>
<br/>
<div>
<!-- <img src="images/1001.png"/> -->
<img src="images/1001.png"/>
</div>
</body>
</html>
var THREEx = THREEx || {}
THREEx.ArucoContext = function(markerSize){
this.canvas = document.createElement('canvas');
this.canvas.width = 80*4
this.canvas.height = 60*4
// experiment with imageSmoothingEnabled
var imageSmoothingEnabled = false
var context = this.canvas.getContext('2d');
context.mozImageSmoothingEnabled = imageSmoothingEnabled;
context.webkitImageSmoothingEnabled = imageSmoothingEnabled;
context.msImageSmoothingEnabled = imageSmoothingEnabled;
context.imageSmoothingEnabled = imageSmoothingEnabled;
this.detector = new AR.Detector();
this.posit = new POS.Posit(markerSize, this.canvas.width);
}
THREEx.ArucoContext.prototype.detect = function (videoElement) {
var _this = this
var canvas = this.canvas
// get imageData from videoElement
var context = canvas.getContext('2d');
context.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
// detect markers in imageData
var detectedMarkers = this.detector.detect(imageData);
// compute the pose for each detectedMarkers
// FIXME: i fix we should have one posit estimator per marker
detectedMarkers.forEach(function(detectedMarker){
var markerCorners = detectedMarker.corners;
// convert the corners
var poseCorners = new Array(markerCorners.length)
for (var i = 0; i < markerCorners.length; ++ i){
var markerCorner = markerCorners[i];
poseCorners[i] = {
x: markerCorner.x - (canvas.width / 2),
y: -markerCorner.y + (canvas.height/ 2)
}
}
// estimate pose from corners
detectedMarker.pose = _this.posit.pose(poseCorners);
})
return detectedMarkers
};
THREEx.ArucoContext.updateObject3D = function(object3D, detectedMarker){
var rotation = detectedMarker.pose.bestRotation
var translation = detectedMarker.pose.bestTranslation
object3D.position.x = translation[0];
object3D.position.y = translation[1];
object3D.position.z = -translation[2];
object3D.rotation.x = -Math.asin(-rotation[1][2]);
object3D.rotation.y = -Math.atan2(rotation[0][2], rotation[2][2]);
object3D.rotation.z = Math.atan2(rotation[1][0], rotation[1][1]);
object3D.scale.x = markerSize;
object3D.scale.y = markerSize;
object3D.scale.z = markerSize;
}
var THREEx = THREEx || {}
THREEx.ArucoMarkerGenerator = function(){
}
THREEx.ArucoMarkerGenerator.createSVG = function(markerId, svgSize){
var domElement = document.createElement('div');
domElement.innerHTML = new ArucoMarker(markerId).toSVG(svgSize);
return domElement
}
THREEx.ArucoMarkerGenerator.createIMG = function(markerId, svgSize){
// get the svgElement
var svgElement = THREEx.ArucoMarkerGenerator.createSVG(markerId, svgSize).firstChild
// build imageURL
var xml = new XMLSerializer().serializeToString(svgElement);
var imageURL = 'data:image/svg+xml;base64,' + btoa(xml)
// create imageElement
var imageElement = document.createElement('img');
imageElement.src = imageURL
// return imageElement
return imageElement;
}
/*! aruco-marker 1.0.0 2014-06-19 - MIT Licensed, see http://github.com/bhollis/aruco-marker */
// Export for use via AMD, Node.js, or a browser global.
// See https://github.com/umdjs/umd/blob/master/returnExportsGlobal.js
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], function () {
return (root.ArucoMarker = factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals
root.ArucoMarker = factory();
}
}(this, function () {
// Create an ArucoMarker object by its ID, which can then be used to generate images.
// The id must be in the range [0..1023]
// Based on https://github.com/rmsalinas/aruco/blob/master/trunk/src/arucofidmarkers.cpp
function ArucoMarker(id) {
if (id < 0 || id > 1023) {
throw new RangeError('Marker ID must be in the range [0..1023]');
}
this.id = id;
}
ArucoMarker.prototype = {
// Generate a marker as a 5x5 matrix of 0s and 1s.
markerMatrix: function() {
var ids = [16, 23, 9, 14];
var index, val, x, y;
var marker = [[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0],
[0, 0, 0, 0, 0]];
for (y = 0; y < 5; y++) {
index = (this.id >> 2 * (4 - y)) & 3;
val = ids[index];
for (x = 0; x < 5; x++) {
if ((val >> (4 - x)) & 1) {
marker[x][y] = 1;
} else {
marker[x][y] = 0;
}
}
}
return marker;
},
// Create an SVG image of the marker, as a string.
// Optionally pass a size (in any SVG-compatible units) or leave it out to size it on your own.
toSVG: function(size) {
var x, y;
var marker = this.markerMatrix();
var image;
if (size) {
size = 'height="' + size + '" width="' + size + '"';
} else {
size = '';
}
image = '<svg ' + size + ' viewBox="0 0 7 7" version="1.1" xmlns="http://www.w3.org/2000/svg">\n' +
' <rect x="0" y="0" width="7" height="7" fill="black"/>\n';
for (y = 0; y < 5; y++) {
for (x = 0; x < 5; x++) {
if (marker[x][y] === 1) {
image += ' <rect x="' + (x + 1) + '" y="' + (y + 1) +
'" width="1" height="1" fill="white" ' +
// Slight stroke to get around aliasing issues with adjacent rectangles
'stroke="white" stroke-width="0.01" />\n';
}
}
}
image += '</svg>';
return image;
}
};
return ArucoMarker;
}));
...@@ -63,6 +63,9 @@ THREEx.ArucoContext.updateObject3D = function(object3D, detectedMarker){ ...@@ -63,6 +63,9 @@ THREEx.ArucoContext.updateObject3D = function(object3D, detectedMarker){
object3D.rotation.y = -Math.atan2(rotation[0][2], rotation[2][2]); object3D.rotation.y = -Math.atan2(rotation[0][2], rotation[2][2]);
object3D.rotation.z = Math.atan2(rotation[1][0], rotation[1][1]); object3D.rotation.z = Math.atan2(rotation[1][0], rotation[1][1]);
// TODO this function must die!!!!
var markerSize = 1
object3D.scale.x = markerSize; object3D.scale.x = markerSize;
object3D.scale.y = markerSize; object3D.scale.y = markerSize;
object3D.scale.z = markerSize; object3D.scale.z = markerSize;
......
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册