Ask Your Question

Revision history [back]

click to hide/show revision 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

image description

A get histogram for HSV And get threshold for brightness and saturation (you can calculate it automatically if you have a peak detector)

image description image description

This is the binary image after some cleaning

image description

Some contour and rotated rect to get orientation and a ROI for the single detection

image description

Finally sort the detections, rotate and extract single images. From quadrant 1

image descriptionimage descriptionimage description

From quadrant 2

image descriptionimage description

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);
}

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

image description

A get histogram for HSV And get threshold for brightness and saturation (you can calculate it automatically if you have a peak detector)

image description image description

This is the binary image after some cleaning

image description

Some contour and rotated rect to get orientation and a ROI for the single detection

image description

Finally sort the detections, rotate and extract single images. From quadrant 1

image descriptionimage descriptionimage description

From quadrant 2

image descriptionimage description

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);
}