1 | initial version |
My attempt considers that painted nails have high brightness and fingers have orientation almost similar. My code can extract images from single nails but it get some noise too. Anyway result could be used as input for a classifier.
As threshold I used the absolute peak for Brightness. I removed some noise using a threshold on Saturation in the middle of 2 biggest peaks
For example starting from this image
A get histogram for HSV And get threshold for brightness and saturation (you can calculate it automatically if you have a peak detector)
This is the binary image after some cleaning
Some contour and rotated rect to get orientation and a ROI for the single detection
Finally sort the detections, rotate and extract single images. From quadrant 1
From quadrant 2
here the full code. (Get FillHoles
from @theodore answer here: http://answers.opencv.org/question/74315/efficient-way-to-remove-holes-in-an-object/?answer=74353#post-id-74353
#define CL_BLU cv::Scalar(255, 0, 0 )
#define CL_GREEN cv::Scalar(0, 255, 0 )
#define CL_RED cv::Scalar(0, 0, 255)
#define CL_BLACK cv::Scalar(0, 0, 0 )
#define CL_CYAN cv::Scalar(255, 255, 0 )
#define CL_MAGENTA cv::Scalar(255, 0, 255)
#define CL_YELLOW cv::Scalar(0, 255, 255)
#define CL_WHITE cv::Scalar(255, 255, 255)
void Nails()
{
cv::Scalar cl;
cv::Mat src, dst, src_hsv, src_saturation, src_bright, bw, dist;
src = cv::imread("../img/nails.jpg");
src.copyTo(dst);
vector<cv::Mat> hsv_planes;
cv::cvtColor(src, src_hsv, cv::COLOR_BGR2HSV);
cv::split(src_hsv, hsv_planes);
src_saturation = hsv_planes[1];
src_bright = hsv_planes[2];
int thBright = 186, thSat=180;
#ifdef TUNA_HIST
cv::Mat hist,histImg;
std::vector<peak>peaks;
std::vector<bool>isPeak;
Hist(src_bright, hist);
DrawHistogram(hist, histImg, 5, 1, "");
FindPeaks(hist, peaks, isPeak, 5);
if (peaks.size() > 0)
thBright = peaks[0].idx ;
int x = cvRound(histImg.cols*thBright / 256);
cv::line(histImg, cv::Point(x, 0), cv::Point(x, histImg.rows), cv::Scalar(0, 0, 255), 2);
cv::putText(histImg, "THRESHOLD", cv::Point(x, 15), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255), 1, CV_AA);
cv::imshow("histBright", histImg);
cv::imwrite("../img/histBright.jpg", histImg);
Hist(src_saturation, hist);
DrawHistogram(hist, histImg, 5, 2, "");
FindPeaks(hist, peaks, isPeak, 5);
if (peaks.size() >= 2)
thSat = cvRound((peaks[0].idx + peaks[1].idx) / 2.0);
x = cvRound(histImg.cols*thSat / 256);
cv::line(histImg, cv::Point(x, 0), cv::Point(x, histImg.rows), cv::Scalar(0, 0, 255), 2);
cv::putText(histImg, "THRESHOLD", cv::Point(x, 15), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255), 1, CV_AA);
cv::imshow("histSaturation", histImg);
cv::imwrite("../img/histSaturation.jpg", histImg);
#endif
cv::inRange(src_hsv, cv::Scalar(0, thSat, thBright), cv::Scalar(180, 255, 255), bw);
FillHoles(bw, bw);
cv::bitwise_not(bw, bw);
Morph(bw, bw, cv::MORPH_CLOSE, cv::MORPH_RECT, 3);
Morph(bw, bw, cv::MORPH_OPEN, cv::MORPH_RECT, 1);
cv::imshow("After TH1", bw);
cv::imwrite("../img/bw.jpg", dst);
// Get external contours ignoring holes
std::vector<std::vector<cv::Point> > contours;
cv::findContours(bw, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
cv::RotatedRect rr;
map <int, vector<cv::RotatedRect>> quadrant;
vector<cv::Scalar> quadrantColor({ CL_MAGENTA, CL_YELLOW, CL_CYAN, CL_GREEN });
double area,minArea = 0.005*bw.cols*bw.rows;
cv::Point2f rect_points[4];
// draw contours, select them and group by orientation
for (int i = 0; i < contours.size(); i++)
{
cv::drawContours(dst, contours, i, CL_WHITE, 1);
//filter out small detections
if (contours[i].size() < 10) continue;
area = cv::contourArea(contours[i]);
if (area < minArea) continue;
// Get a rotated rect around the detection
rr = cv::fitEllipse(contours[i]);
// group detections by orientation in quadrant
// because there is a good chance that all fingers
// have same orientation.
int q = floor(rr.angle / 90);
quadrant[q].push_back(rr);
// Draw rotated rect around the detection
cl = quadrantColor[q];
rr.points(rect_points);
for (int j = 0; j < 4; j++)
cv::line(dst, rect_points[j], rect_points[(j + 1) % 4], cl, 2, 8);
cv::putText(dst, "Q" + to_string(q)+"C" + to_string(i), rr.center - cv::Point2f(3, 0), cv::FONT_HERSHEY_PLAIN, 1, CL_BLACK, 2,CV_AA);
}
// Get quadrant with more detections count
// you could consider this as related to fingers
int cnt = 0;
int populatedQuadrant = -1;
for (int i = 0; i < quadrant.size(); i++)
{
if (quadrant[i].size()>cnt)
{
cnt = quadrant[i].size();
populatedQuadrant = i;
}
}
vector<cv::RotatedRect> nails;
vector<vector<int>> nailsOrder(quadrant.size());
// for each quadrant sort detections sequence using distance
// fingers should be ordered from 1st to last
for (int idx = 0; idx < quadrant.size(); idx++)
{
nails = quadrant[idx];
float nextDist, prevDist, minPrev = 100000000, minNext = 100000000;
for (int i = 0; i < nails.size(); i++)
{
int next = -1, prev = -1;
if ((nailsOrder[idx].size()>0) && (i == nailsOrder[idx].back()))
continue;
for (int j = i + 1; j < nails.size() - 1; j++)
{
nextDist = cv::norm(nails[i].center - nails[j].center);
if (nextDist < minNext)
{
next = j;
minNext = nextDist;
}
}
if (prev >= 0)
nailsOrder[idx].push_back(prev);
nailsOrder[idx].push_back(i);
if (next >= 0)
nailsOrder[idx].push_back(next);
}
}
// for each quadrant
cv::Mat theNail, src_rotated;
cv::Mat rot_mat(2, 3, CV_32FC1);
string txtQ, txt;
for (int idx = 0; idx < quadrant.size(); idx++)
{
// get the detection list in this quadrant
nails = quadrant[idx];
txtQ = "Q" + to_string(idx) + "N";
// for each detections cut the image in the middle
// of distance between 2 consecutive detections
// rotate the detection and extract an image
float nailDist = 0;
for (int i = 0; i < nailsOrder[idx].size(); i++)
{
int curr = nailsOrder[idx][i];
txt = txtQ + to_string(curr);
//take care for last detection
if (i < nailsOrder[idx].size() - 1)
{
int next = nailsOrder[idx][i + 1];
nailDist = norm(nails[curr].center - nails[next].center);
}
if (nailDist == 0)
nailDist = nails[curr].size.width;
// Rotate full image around center of detection
// the center of detection will remain valid
rot_mat = cv::getRotationMatrix2D(nails[curr].center, nails[curr].angle, 1);
cv::Size newSize = cv::Size(0, 0) + src.size();
warpAffine(src, src_rotated, rot_mat, newSize);
// Create a ROI around the center using distance from the next
circle(src_rotated, nails[curr].center, 3, 0, -1);
double k = nailDist / nails[curr].size.width;
double h = 0.7*k*nails[curr].size.height;
double w = nailDist;
cv::Rect nailROI;
nailROI = cv::Rect(nails[curr].center.x - w / 2, nails[curr].center.y - h / 2, w, h);
nailROI = nailROI & cv::Rect(cv::Point(0, 0), src_rotated.size());
theNail = src_rotated(nailROI);
cv::putText(theNail, txt, cv::Point2f(10, 10), cv::FONT_HERSHEY_PLAIN, 1, CL_YELLOW, 2, CV_AA);
cv::imshow(txt, theNail);
cv::imwrite("../img/"+txt+".jpg", dst);
}
}
cv::imshow("Dst", dst);
cv::imwrite("../dst.jpg", dst);
}
2 | No.2 Revision |
My attempt considers that painted nails have high brightness and fingers have orientation almost similar. My code can extract images from single nails but it get some noise too. Anyway result could be used as input for a classifier.
As threshold I used the absolute peak for Brightness. I removed some noise using a threshold on Saturation in the middle of 2 biggest peaks
For example starting from this image
A get histogram for HSV And get threshold for brightness and saturation (you can calculate it automatically if you have a peak detector)
This is the binary image after some cleaning
Some contour and rotated rect to get orientation and a ROI for the single detection
Finally sort the detections, rotate and extract single images. From quadrant 1
From quadrant 2
here the full code. (Get FillHoles
from @theodore answer here: http://answers.opencv.org/question/74315/efficient-way-to-remove-holes-in-an-object/?answer=74353#post-id-74353
#define CL_BLU cv::Scalar(255, 0, 0 )
#define CL_GREEN cv::Scalar(0, 255, 0 )
#define CL_RED cv::Scalar(0, 0, 255)
#define CL_BLACK cv::Scalar(0, 0, 0 )
#define CL_CYAN cv::Scalar(255, 255, 0 )
#define CL_MAGENTA cv::Scalar(255, 0, 255)
#define CL_YELLOW cv::Scalar(0, 255, 255)
#define CL_WHITE cv::Scalar(255, 255, 255)
void Nails()
{
cv::Scalar cl;
cv::Mat src, dst, src_hsv, src_saturation, src_bright, bw, dist;
src = cv::imread("../img/nails.jpg");
src.copyTo(dst);
vector<cv::Mat> hsv_planes;
cv::cvtColor(src, src_hsv, cv::COLOR_BGR2HSV);
cv::split(src_hsv, hsv_planes);
src_saturation = hsv_planes[1];
src_bright = hsv_planes[2];
int thBright = 186, thSat=180;
#ifdef TUNA_HIST
cv::Mat hist,histImg;
std::vector<peak>peaks;
std::vector<bool>isPeak;
Hist(src_bright, hist);
DrawHistogram(hist, histImg, 5, 1, "");
FindPeaks(hist, peaks, isPeak, 5);
if (peaks.size() > 0)
thBright = peaks[0].idx ;
int x = cvRound(histImg.cols*thBright / 256);
cv::line(histImg, cv::Point(x, 0), cv::Point(x, histImg.rows), cv::Scalar(0, 0, 255), 2);
cv::putText(histImg, "THRESHOLD", cv::Point(x, 15), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255), 1, CV_AA);
cv::imshow("histBright", histImg);
cv::imwrite("../img/histBright.jpg", histImg);
Hist(src_saturation, hist);
DrawHistogram(hist, histImg, 5, 2, "");
FindPeaks(hist, peaks, isPeak, 5);
if (peaks.size() >= 2)
thSat = cvRound((peaks[0].idx + peaks[1].idx) / 2.0);
x = cvRound(histImg.cols*thSat / 256);
cv::line(histImg, cv::Point(x, 0), cv::Point(x, histImg.rows), cv::Scalar(0, 0, 255), 2);
cv::putText(histImg, "THRESHOLD", cv::Point(x, 15), cv::FONT_HERSHEY_PLAIN, 1, cv::Scalar(0, 0, 255), 1, CV_AA);
cv::imshow("histSaturation", histImg);
cv::imwrite("../img/histSaturation.jpg", histImg);
#endif
cv::inRange(src_hsv, cv::Scalar(0, thSat, thBright), cv::Scalar(180, 255, 255), bw);
FillHoles(bw, bw);
cv::bitwise_not(bw, bw);
Morph(bw, bw, cv::MORPH_CLOSE, cv::MORPH_RECT, 3);
Morph(bw, bw, cv::MORPH_OPEN, cv::MORPH_RECT, 1);
cv::imshow("After TH1", bw);
cv::imwrite("../img/bw.jpg", dst);
// Get external contours ignoring holes
std::vector<std::vector<cv::Point> > contours;
cv::findContours(bw, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
cv::RotatedRect rr;
map <int, vector<cv::RotatedRect>> quadrant;
vector<cv::Scalar> quadrantColor({ CL_MAGENTA, CL_YELLOW, CL_CYAN, CL_GREEN });
double area,minArea = 0.005*bw.cols*bw.rows;
cv::Point2f rect_points[4];
// draw contours, select them and group by orientation
for (int i = 0; i < contours.size(); i++)
{
cv::drawContours(dst, contours, i, CL_WHITE, 1);
//filter out small detections
if (contours[i].size() < 10) continue;
area = cv::contourArea(contours[i]);
if (area < minArea) continue;
// Get a rotated rect around the detection
rr = cv::fitEllipse(contours[i]);
// group detections by orientation in quadrant
// because there is a good chance that all fingers
// have same orientation.
int q = floor(rr.angle / 90);
quadrant[q].push_back(rr);
// Draw rotated rect around the detection
cl = quadrantColor[q];
rr.points(rect_points);
for (int j = 0; j < 4; j++)
cv::line(dst, rect_points[j], rect_points[(j + 1) % 4], cl, 2, 8);
cv::putText(dst, "Q" + to_string(q)+"C" + to_string(i), rr.center - cv::Point2f(3, 0), cv::FONT_HERSHEY_PLAIN, 1, CL_BLACK, 2,CV_AA);
}
// Get quadrant with more detections count
// you could consider this as related to fingers
int cnt = 0;
int populatedQuadrant = -1;
for (int i = 0; i < quadrant.size(); i++)
{
if (quadrant[i].size()>cnt)
{
cnt = quadrant[i].size();
populatedQuadrant = i;
}
}
vector<cv::RotatedRect> nails;
vector<vector<int>> nailsOrder(quadrant.size());
// for each quadrant sort detections sequence using distance
// fingers should be ordered from 1st to last
for (int idx = 0; idx < quadrant.size(); idx++)
{
nails = quadrant[idx];
float nextDist, prevDist, minPrev = 100000000, minNext = 100000000;
for (int i = 0; i < nails.size(); i++)
{
int next = -1, prev = -1;
if ((nailsOrder[idx].size()>0) && (i == nailsOrder[idx].back()))
continue;
for (int j = i + 1; j < nails.size() - 1; j++)
{
nextDist = cv::norm(nails[i].center - nails[j].center);
if (nextDist < minNext)
{
next = j;
minNext = nextDist;
}
}
if (prev >= 0)
nailsOrder[idx].push_back(prev);
nailsOrder[idx].push_back(i);
if (next >= 0)
nailsOrder[idx].push_back(next);
}
}
// for each quadrant
cv::Mat theNail, src_rotated;
cv::Mat rot_mat(2, 3, CV_32FC1);
string txtQ, txt;
for (int idx = 0; idx < quadrant.size(); idx++)
{
// get the detection list in this quadrant
nails = quadrant[idx];
txtQ = "Q" + to_string(idx) + "N";
// for each detections cut the image in the middle
// of distance between 2 consecutive detections
// rotate the detection and extract an image
float nailDist = 0;
for (int i = 0; i < nailsOrder[idx].size(); i++)
{
int curr = nailsOrder[idx][i];
txt = txtQ + to_string(curr);
//take care for last detection
if (i < nailsOrder[idx].size() - 1)
{
int next = nailsOrder[idx][i + 1];
nailDist = norm(nails[curr].center - nails[next].center);
}
if (nailDist == 0)
nailDist = nails[curr].size.width;
// Rotate full image around center of detection
// the center of detection will remain valid
rot_mat = cv::getRotationMatrix2D(nails[curr].center, nails[curr].angle, 1);
cv::Size newSize = cv::Size(0, 0) + src.size();
warpAffine(src, src_rotated, rot_mat, newSize);
// Create a ROI around the center using distance from the next
circle(src_rotated, nails[curr].center, 3, 0, -1);
double k = nailDist / nails[curr].size.width;
double h = 0.7*k*nails[curr].size.height;
double w = nailDist;
cv::Rect nailROI;
nailROI = cv::Rect(nails[curr].center.x - w / 2, nails[curr].center.y - h / 2, w, h);
nailROI = nailROI & cv::Rect(cv::Point(0, 0), src_rotated.size());
theNail = src_rotated(nailROI);
cv::putText(theNail, txt, cv::Point2f(10, 10), cv::FONT_HERSHEY_PLAIN, 1, CL_YELLOW, 2, CV_AA);
cv::imshow(txt, theNail);
cv::imwrite("../img/"+txt+".jpg", dst);
}
}
cv::imshow("Dst", dst);
cv::imwrite("../dst.jpg", dst);
}