Commit 1a55d83d authored by Nadir Dalla Pozza's avatar Nadir Dalla Pozza
Browse files

Automatic reading head and capstan detection. Tape area derivative from reading head.

parent 6a90b476
/usr/bin/clang++ -g -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -mmacosx-version-min=12.6 -Wl,-search_paths_first -Wl,-headerpad_max_install_names CMakeFiles/video_analyser.dir/src/script.cpp.o -o /Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/bin/video_analyser -L/usr/local/lib -Wl,-rpath,/usr/local/lib -Wl,-rpath,/usr/local/opt/opencv/lib /usr/local/opt/opencv/lib/libopencv_gapi.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_stitching.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_alphamat.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_aruco.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_barcode.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_bgsegm.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_bioinspired.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_ccalib.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_dnn_objdetect.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_dnn_superres.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_dpm.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_face.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_freetype.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_fuzzy.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_hfs.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_img_hash.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_intensity_transform.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_line_descriptor.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_mcc.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_quality.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_rapid.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_reg.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_rgbd.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_saliency.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_sfm.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_stereo.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_structured_light.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_superres.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_surface_matching.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_tracking.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_videostab.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_viz.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_wechat_qrcode.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_xfeatures2d.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_xobjdetect.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_xphoto.4.7.0.dylib /usr/local/lib/libboost_program_options.dylib /usr/local/opt/opencv/lib/libopencv_shape.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_highgui.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_datasets.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_plot.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_text.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_ml.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_phase_unwrapping.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_optflow.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_ximgproc.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_video.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_videoio.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_imgcodecs.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_objdetect.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_calib3d.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_dnn.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_features2d.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_flann.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_photo.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_imgproc.4.7.0.dylib /usr/local/opt/opencv/lib/libopencv_core.4.7.0.dylib
# CMAKE generated file: DO NOT EDIT!
# Generated by "Unix Makefiles" Generator, CMake Version 3.23
# Generated by "Unix Makefiles" Generator, CMake Version 3.25
# Default target executed when no arguments are given to make.
default_target: all
......@@ -48,10 +48,10 @@ cmake_force:
SHELL = /bin/sh
# The CMake executable.
CMAKE_COMMAND = /usr/local/Cellar/cmake/3.23.2/bin/cmake
CMAKE_COMMAND = /usr/local/Cellar/cmake/3.25.1/bin/cmake
# The command to remove a file.
RM = /usr/local/Cellar/cmake/3.23.2/bin/cmake -E rm -f
RM = /usr/local/Cellar/cmake/3.25.1/bin/cmake -E rm -f
# Escaping for special characters.
EQUALS = =
......@@ -68,7 +68,7 @@ CMAKE_BINARY_DIR = /Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/build
# Special rule for the target edit_cache
edit_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Running CMake cache editor..."
/usr/local/Cellar/cmake/3.23.2/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
/usr/local/Cellar/cmake/3.25.1/bin/ccmake -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : edit_cache
# Special rule for the target edit_cache
......@@ -78,7 +78,7 @@ edit_cache/fast: edit_cache
# Special rule for the target rebuild_cache
rebuild_cache:
@$(CMAKE_COMMAND) -E cmake_echo_color --switch=$(COLOR) --cyan "Running CMake to regenerate build system..."
/usr/local/Cellar/cmake/3.23.2/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
/usr/local/Cellar/cmake/3.25.1/bin/cmake --regenerate-during-build -S$(CMAKE_SOURCE_DIR) -B$(CMAKE_BINARY_DIR)
.PHONY : rebuild_cache
# Special rule for the target rebuild_cache
......@@ -117,24 +117,24 @@ depend:
.PHONY : depend
#=============================================================================
# Target rules for targets named frame_extraction
# Target rules for targets named video_analyser
# Build rule for target.
frame_extraction: cmake_check_build_system
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 frame_extraction
.PHONY : frame_extraction
video_analyser: cmake_check_build_system
$(MAKE) $(MAKESILENT) -f CMakeFiles/Makefile2 video_analyser
.PHONY : video_analyser
# fast build rule for target.
frame_extraction/fast:
$(MAKE) $(MAKESILENT) -f CMakeFiles/frame_extraction.dir/build.make CMakeFiles/frame_extraction.dir/build
.PHONY : frame_extraction/fast
video_analyser/fast:
$(MAKE) $(MAKESILENT) -f CMakeFiles/video_analyser.dir/build.make CMakeFiles/video_analyser.dir/build
.PHONY : video_analyser/fast
src/script.o: src/script.cpp.o
.PHONY : src/script.o
# target to build an object file
src/script.cpp.o:
$(MAKE) $(MAKESILENT) -f CMakeFiles/frame_extraction.dir/build.make CMakeFiles/frame_extraction.dir/src/script.cpp.o
$(MAKE) $(MAKESILENT) -f CMakeFiles/video_analyser.dir/build.make CMakeFiles/video_analyser.dir/src/script.cpp.o
.PHONY : src/script.cpp.o
src/script.i: src/script.cpp.i
......@@ -142,7 +142,7 @@ src/script.i: src/script.cpp.i
# target to preprocess a source file
src/script.cpp.i:
$(MAKE) $(MAKESILENT) -f CMakeFiles/frame_extraction.dir/build.make CMakeFiles/frame_extraction.dir/src/script.cpp.i
$(MAKE) $(MAKESILENT) -f CMakeFiles/video_analyser.dir/build.make CMakeFiles/video_analyser.dir/src/script.cpp.i
.PHONY : src/script.cpp.i
src/script.s: src/script.cpp.s
......@@ -150,7 +150,7 @@ src/script.s: src/script.cpp.s
# target to generate assembly for a file
src/script.cpp.s:
$(MAKE) $(MAKESILENT) -f CMakeFiles/frame_extraction.dir/build.make CMakeFiles/frame_extraction.dir/src/script.cpp.s
$(MAKE) $(MAKESILENT) -f CMakeFiles/video_analyser.dir/build.make CMakeFiles/video_analyser.dir/src/script.cpp.s
.PHONY : src/script.cpp.s
# Help Target
......@@ -161,7 +161,7 @@ help:
@echo "... depend"
@echo "... edit_cache"
@echo "... rebuild_cache"
@echo "... frame_extraction"
@echo "... video_analyser"
@echo "... src/script.o"
@echo "... src/script.i"
@echo "... src/script.s"
......
[
{
"directory": "/Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/build",
"command": "/usr/bin/clang++ -DJSON_DIAGNOSTICS=0 -DJSON_USE_IMPLICIT_CONVERSIONS=1 -isystem /usr/local/Cellar/opencv/4.5.5_2/include/opencv4 -isystem /usr/local/include -std=c++11 -g -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX12.3.sdk -std=gnu++11 -o CMakeFiles/frame_extraction.dir/src/script.cpp.o -c /Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/src/script.cpp",
"command": "/usr/bin/clang++ -DBOOST_PROGRAM_OPTIONS_DYN_LINK -DBOOST_PROGRAM_OPTIONS_NO_LIB -isystem /usr/local/Cellar/opencv/4.7.0_1/include/opencv4 -isystem /usr/local/include -g -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX13.1.sdk -mmacosx-version-min=12.6 -std=gnu++2b -o CMakeFiles/video_analyser.dir/src/script.cpp.o -c /Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/src/script.cpp",
"file": "/Users/nadir/Documents/MPAI-CAE/AIMs/VideoAnalyser/src/script.cpp"
}
]
\ No newline at end of file
{
"WorkingPath": "/Users/nadir/Documents/MPAI-CAE/Workflow",
"FilesName": "test.mov",
"Brands": true,
"IrregularityFileInput": "../input/BERIO060.mov_IrregularityFileInput.json",
"OutputPath": "../output/",
"PreservationAudioVisualFile": "../input/BERIO060.mov",
"Speed": 7.5,
"ThresholdPercentualTape": 77,
"ThresholdPercentualCapstan": 93,
"TapeThresholdPercentual": 80,
"CapstanThresholdPercentual": 50,
"MinDist": 10,
"AngleThresh": 10000,
"ScaleThresh": 200,
......
{
"Irregularities": [
{
"IrregularityID": "4430235c-9640-4c0a-bfe9-3c2518af9d8a",
"Source": "a",
"TimeLabel": "00:02:13.880"
}
]
}
\ No newline at end of file
This diff is collapsed.
namespace fs = std::__fs::filesystem;
using namespace cv;
using namespace std;
std::string pathBrand, pathFullFrame, pathTape, pathTape224, pathEndTape;
/* ------------------------------------------------------------------------------
FILE SYSTEM FUNCTIONS
------------------------------------------------------------------------------ */
void makeDirectories(std::string fileName, std::string outputPath, bool brands) {
// Make output directories
pathBrand = outputPath + "/brand";
pathFullFrame = outputPath + "/fullFrame";
pathTape = outputPath + "/tape";
pathTape224 = outputPath + "/tape224";
pathEndTape = outputPath + "/endTape";
if (brands)
int brandDirectory = fs::create_directory(pathBrand);
int fullFrameDirectory = fs::create_directory(pathFullFrame);
int nastroDirectory = fs::create_directory(pathTape);
int nastro224Directory = fs::create_directory(pathTape224);
int capsDirectory = fs::create_directory(pathEndTape);
}
void saveIrregularityImage(std::string safeTimeLabel, std::string fileName, cv::Mat frame, cv::Mat subFrame, bool endFrame, bool brandFrame) {
// if (savingPinchRoller) {
// cv::imwrite(pathEndTape + "/" + fileName + "_" + safeTimeLabel + ".jpg", frame);
// } else if (savingBrand) {
// cv::imwrite(pathBrand + "/" + fileName + "_" + safeTimeLabel + ".jpg", frame);
// savingBrand = false;
// } else {
// Writing image
cv::imwrite(pathFullFrame + "/" + fileName + "_" + safeTimeLabel + ".jpg", frame);
if (endFrame) {
cv::imwrite(pathEndTape + "/" + fileName + "_" + safeTimeLabel + ".jpg", subFrame);
} else if (brandFrame) {
cv::imwrite(pathBrand + "/" + fileName + "_" + safeTimeLabel + ".jpg", subFrame);
} else {
cv::imwrite(pathTape + "/" + fileName + "_" + safeTimeLabel + ".jpg", subFrame);
}
cv::Mat resized224Nas;
cv::resize(subFrame, resized224Nas, cv::Size(224, 224));
cv::imwrite(pathTape224 + "/"+ fileName + "_" + safeTimeLabel + ".jpg", resized224Nas);
// }
}
namespace fs = std::filesystem;
/* ------------------------------------------------------------------------------
TIME FUNCTIONS
------------------------------------------------------------------------------ */
std::string getTimeLabel(int ms) {
string getTimeLabel(int ms) {
int mil = ms % 1000;
int sec = ms / 1000;
int min = (sec / 60) % 60;
int hours = sec / 3600;
sec = sec % 60;
std::string hoursStr = std::to_string(hours), minStr = std::to_string(min), secStr = std::to_string(sec), milStr = std::to_string(mil);
string hoursStr = to_string(hours), minStr = to_string(min), secStr = to_string(sec), milStr = to_string(mil);
if (hours < 10)
hoursStr = "0" + hoursStr;
if (min < 10)
......@@ -78,8 +29,8 @@ std::string getTimeLabel(int ms) {
milStr = "0" + milStr;
}
}
std::string timeLabel = hoursStr + ":" + minStr + ":" + secStr + "." + milStr;
string timeLabel = hoursStr + ":" + minStr + ":" + secStr + "." + milStr;
return timeLabel;
......@@ -87,15 +38,15 @@ std::string getTimeLabel(int ms) {
// This function is exactly like getTimeLabel, but without punctuation in the returned string
// due to file system necessities
std::string getSafeTimeLabel(int ms) {
string getSafeTimeLabel(int ms) {
int mil = ms % 1000;
int sec = ms / 1000;
int min = (sec / 60) % 60;
int hours = sec / 3600;
sec = sec % 60;
std::string hoursStr = std::to_string(hours), minStr = std::to_string(min), secStr = std::to_string(sec), milStr = std::to_string(mil);
string hoursStr = to_string(hours), minStr = to_string(min), secStr = to_string(sec), milStr = to_string(mil);
if (hours < 10)
hoursStr = "0" + hoursStr;
if (min < 10)
......@@ -109,26 +60,11 @@ std::string getSafeTimeLabel(int ms) {
milStr = "0" + milStr;
}
}
std::string timeLabel = hoursStr + "-" + minStr + "-" + secStr + "-" + milStr;
return timeLabel;
}
int getMilliCount() {
timeb tb;
ftime(&tb);
int nCount = tb.millitm + (tb.time & 0xfffff) * 1000;
return nCount;
}
string timeLabel = hoursStr + "-" + minStr + "-" + secStr + "-" + milStr;
return timeLabel;
int getMilliSpan(int nTimeStart) {
int nSpan = getMilliCount() - nTimeStart;
if (nSpan < 0)
nSpan += 0x100000 * 1000;
return nSpan;
}
......@@ -136,26 +72,26 @@ int getMilliSpan(int nTimeStart) {
PARSING FUNCTIONS
------------------------------------------------------------------------------ */
void findFileNameFromPath(std::string* path, std::string* fileName, std::string* extension) {
*path = path->substr(path->find_last_of("'") + 1, path->size());
std::string path_without_extension = path->substr(0, path->find_last_of("."));
*fileName = path_without_extension.substr(path_without_extension.find_last_of("/") + 1, path_without_extension.size());
*extension = path->substr(path->find_last_of(".") + 1, path->size());
void findFileNameFromPath(string* path, string* fileName, string* extension) {
*path = path->substr(path->find_last_of("'") + 1, path->size());
string path_without_extension = path->substr(0, path->find_last_of("."));
*fileName = path_without_extension.substr(path_without_extension.find_last_of("/") + 1, path_without_extension.size());
*extension = path->substr(path->find_last_of(".") + 1, path->size());
}
// Obtain video file name without path
int findFileName(std::string videoPath, std::string &fileName, std::string &extension) {
int findFileName(string videoPath, string &fileName, string &extension) {
// Divide file name from extension
findFileNameFromPath(&videoPath, &fileName, &extension);
if (extension.compare("avi") != 0 && extension.compare("mp4") != 0 && extension.compare("mov") != 0) {
std::cerr << "Input file extension must be \"avi\", \"mp4\" or \"mov\"." << std::endl;
cerr << "Input file extension must be \"avi\", \"mp4\" or \"mov\"." << endl;
return -1;
} else {
std::cout << "\nVideo to be analysed: " << std::endl;
std::cout << " File name: " << fileName << std::endl;
std::cout << " Extension: " << extension << std::endl;
cout << "Video to be analysed: " << endl;
cout << " File name: " << fileName << endl;
cout << " Extension: " << extension << endl;
}
return 0;
}
......@@ -167,7 +103,7 @@ COMPUTER VISION FUNCTIONS
// Function to detect shape in frame
void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh, vector<Vec4f> &positivePositions, Mat &positiveVotes, vector<Vec4f> &negativePositions, Mat &negativeVotes, Mat processingArea) {
alg -> setPosThresh(posThresh);
alg -> setTemplate(templateShape);
......@@ -177,8 +113,7 @@ void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh
// Process shapes with positive angles
alg -> setMinAngle(0);
alg -> setMaxAngle(5);
std::cout << "Processing positive angles..." << endl;
alg -> setMaxAngle(3);
while (true) {
alg -> detect(processingArea, positivePositions, positiveVotes);
int currentSize = positivePositions.size();
......@@ -192,7 +127,6 @@ void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh
break;
} else if (currentSize == 0 && oldSizePositive == 0) {
// Impossible to find with these parameters
std::cout << "\033[0;31mNot found for positive angles.\033[0m" << endl;
break;
}
oldSizePositive = currentSize;
......@@ -217,9 +151,8 @@ void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh
i = 0;
maxVote = 0;
// Process shapes with negative angles
alg -> setMinAngle(355);
alg -> setMinAngle(357);
alg -> setMaxAngle(360);
std::cout << "Processing negative angles..." << endl;
while (true) {
alg -> detect(processingArea, negativePositions, negativeVotes);
int currentSize = negativePositions.size();
......@@ -233,7 +166,6 @@ void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh
break;
} else if (currentSize == 0 && oldSizeNegative == 0) {
// Impossible to found with these parameters
std::cout << "\033[0;31mNot found for negative angles.\033[0m" << endl;
break;
}
oldSizeNegative = currentSize;
......@@ -256,27 +188,25 @@ void detectShape(Ptr<GeneralizedHoughGuil> alg, Mat templateShape, int posThresh
}
// Function to draw detected shapes in a frame
RotatedRect drawShapes(Mat frame, vector<Vec4f> &positions, Scalar color, int width, int height, int offsetX, int offsetY, float processingScale) {
RotatedRect drawShapes(Mat frame, Vec4f &positions, Scalar color, int width, int height, int offsetX, int offsetY, float processingScale) {
RotatedRect rr;
Point2f rrpts[4];
for (int i = 0; i < positions.size(); i++) {
Point2f pos(positions[i][0]+offsetX, positions[i][1]+offsetY);
float scale = positions[i][2];
float angle = positions[i][3];
Point2f pos(positions[0]+offsetX, positions[1]+offsetY);
float scale = positions[2];
float angle = positions[3];
rr.center = pos * processingScale;
rr.size = Size2f(width * scale * processingScale, height * scale * processingScale);
rr.angle = angle;
rr.center = pos * processingScale;
rr.size = Size2f(width * scale * processingScale, height * scale * processingScale);
rr.angle = angle;
rr.points(rrpts);
rr.points(rrpts);
line(frame, rrpts[0], rrpts[1], color, 2);
line(frame, rrpts[1], rrpts[2], color, 2);
line(frame, rrpts[2], rrpts[3], color, 2);
line(frame, rrpts[3], rrpts[0], color, 2);
}
line(frame, rrpts[0], rrpts[1], color, 2);
line(frame, rrpts[1], rrpts[2], color, 2);
line(frame, rrpts[2], rrpts[3], color, 2);
line(frame, rrpts[3], rrpts[0], color, 2);
return rr;
}
......@@ -311,33 +241,6 @@ void separateFrame(cv::Mat frame, cv::Mat &frame_dispari, cv::Mat &frame_pari) {
}
// This function finds the straight lines delimiting the rotated rectangle.
void findRectBound(cv::Point p1, cv::Point p2, cv::Point p3, cv::Point p4, double &m1, double &m2, double &m3, double &m4, double &q1, double &q2, double &q3, double &q4) {
int y1 = p1.y;
int y2 = p2.y;
int y3 = p3.y;
int y4 = p4.y;
int x1 = p1.x;
int x2 = p2.x;
int x3 = p3.x;
int x4 = p4.x;
// Finding the straight lines intersecting in y1 [p0.y]
m1 = (double)(y2 - y1)/(x2 - x1);
q1 = (y1 - m1*x1);
m4 = (double)(y4 - y1)/(x4 - x1);
q4 = (y1 - m4*x1);
// Finding the straight lines intersecting in y3 [p2.y]
m2 = (double)(y3 - y2)/(x3 - x2);
q2 = (y3 - m2*x3);
m3= (double)(y4 - y3)/(x4 - x3);
q3 = (y3 - m3*x3);
}
cv::Mat difference(cv::Mat &prevFrame, cv::Mat &currentFrame) {
cv::Mat diff = currentFrame.clone();
for (int i = 0; i < currentFrame.rows; i++) {
......@@ -352,5 +255,4 @@ cv::Mat difference(cv::Mat &prevFrame, cv::Mat &currentFrame) {
}
}
return diff;
}
}
\ No newline at end of file
Supports Markdown
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