293 lines
10 KiB
C++
293 lines
10 KiB
C++
#include <iostream>
|
|
#include <vector>
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <opencv2/opencv.hpp>
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
namespace ColorProcessing {
|
|
|
|
// 1. Color Complement / Negative (Section 6.5)
|
|
cv::Mat computeComplement(const cv::Mat& input) {
|
|
cv::Mat output;
|
|
// Operasi vektor element-wise: s_i = 255 - r_i
|
|
cv::bitwise_not(input, output);
|
|
return output;
|
|
}
|
|
|
|
// 2. Tonal Contrast Adjustment menggunakan kurva S (Section 6.5, Fig 6.33)
|
|
cv::Mat adjustToneSCurve(const cv::Mat& input) {
|
|
cv::Mat lut(1, 256, CV_8U);
|
|
uchar* ptr = lut.ptr();
|
|
|
|
// Membentuk kurva S halus: f(x) = 255 / (1 + exp(-k * (x - 128)))
|
|
// Dinormalisasi agar f(0) = 0 dan f(255) = 255
|
|
const double k = 0.035;
|
|
auto sigmoid = [k](double x) { return 1.0 / (1.0 + std::exp(-k * (x - 128.0))); };
|
|
const double lo = sigmoid(0.0);
|
|
const double hi = sigmoid(255.0);
|
|
for (int i = 0; i < 256; ++i) {
|
|
double val = 255.0 * (sigmoid(i) - lo) / (hi - lo);
|
|
ptr[i] = cv::saturate_cast<uchar>(val);
|
|
}
|
|
|
|
cv::Mat output;
|
|
cv::LUT(input, lut, output);
|
|
return output;
|
|
}
|
|
|
|
// 3. Histogram Equalization pada kanal Value di ruang HSV (pendekatan kanal intensitas HSI) (Section 6.5, Fig 6.35)
|
|
cv::Mat equalizeIntensityHSI(const cv::Mat& input) {
|
|
cv::Mat hsv;
|
|
cv::cvtColor(input, hsv, cv::COLOR_BGR2HSV);
|
|
|
|
std::vector<cv::Mat> channels;
|
|
cv::split(hsv, channels);
|
|
|
|
// Hanya ratakan kanal Value/Intensity (index 2)
|
|
// agar hue dan saturation tidak terdistorsi menjadi warna palsu
|
|
cv::equalizeHist(channels[2], channels[2]);
|
|
|
|
// Sedikit optimasi saturasi seperti di Example 6.11
|
|
channels[1] = channels[1] * 1.15;
|
|
|
|
cv::Mat merged;
|
|
cv::merge(channels, merged);
|
|
|
|
cv::Mat output;
|
|
cv::cvtColor(merged, output, cv::COLOR_HSV2BGR);
|
|
return output;
|
|
}
|
|
|
|
// 4. Color Slicing berbasis bola Euclidean 3D (Section 6.5, Eq. 6-44)
|
|
// Mengisolasi warna target (default: warna kulit/hangat) dan mengubah sisanya ke abu-abu netral
|
|
cv::Mat sliceColorSphere(const cv::Mat& input, const cv::Vec3b& prototypeColor, double radius) {
|
|
cv::Mat output = input.clone();
|
|
const double radiusSq = radius * radius;
|
|
const cv::Vec3b neutralGray(128, 128, 128);
|
|
|
|
// Akses data berurutan untuk memaksimalkan cache locality
|
|
for (int r = 0; r < input.rows; ++r) {
|
|
const cv::Vec3b* inRow = input.ptr<cv::Vec3b>(r);
|
|
cv::Vec3b* outRow = output.ptr<cv::Vec3b>(r);
|
|
|
|
for (int c = 0; c < input.cols; ++c) {
|
|
double db = static_cast<double>(inRow[c][0]) - prototypeColor[0];
|
|
double dg = static_cast<double>(inRow[c][1]) - prototypeColor[1];
|
|
double dr = static_cast<double>(inRow[c][2]) - prototypeColor[2];
|
|
|
|
double distSq = (dr * dr) + (dg * dg) + (db * db);
|
|
|
|
if (distSq > radiusSq) {
|
|
outRow[c] = neutralGray;
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// 5. Sharpening Vektor Warna via Laplacian Operator (Section 6.6, Eq. 6-47)
|
|
cv::Mat sharpenLaplacian(const cv::Mat& input) {
|
|
cv::Mat laplacian, output;
|
|
|
|
// Matriks kernel Laplacian 3x3 standar
|
|
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
|
|
0, -1, 0,
|
|
-1, 5, -1,
|
|
0, -1, 0);
|
|
|
|
cv::filter2D(input, output, input.depth(), kernel);
|
|
return output;
|
|
}
|
|
|
|
// 6. Pseudocolor Processing (Section 6.3)
|
|
cv::Mat generatePseudocolor(const cv::Mat& input) {
|
|
cv::Mat gray, output;
|
|
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
|
|
cv::applyColorMap(gray, output, cv::COLORMAP_JET);
|
|
return output;
|
|
}
|
|
|
|
// 7. Deteksi wajah dengan Haar cascade
|
|
std::vector<cv::Rect> detectFaces(const cv::Mat& input, cv::CascadeClassifier& cascade) {
|
|
cv::Mat gray;
|
|
cv::cvtColor(input, gray, cv::COLOR_BGR2GRAY);
|
|
// CLAHE (equalisasi lokal) lebih tahan terhadap pencahayaan tidak merata daripada equalizeHist global
|
|
cv::createCLAHE(3.0, cv::Size(8, 8))->apply(gray, gray);
|
|
|
|
std::vector<cv::Rect> faces;
|
|
cascade.detectMultiScale(gray, faces, 1.1, 5, 0, cv::Size(40, 40));
|
|
return faces;
|
|
}
|
|
|
|
// 8. Ubah warna kulit wajah: rotasi Hue + skala Saturation pada piksel kulit di dalam wajah (Section 6.2, 6.5)
|
|
// Mask = rentang kulit di YCrCb AND elips di dalam kotak wajah, tepi di-blur agar transisi halus
|
|
cv::Mat recolorFace(const cv::Mat& input, const std::vector<cv::Rect>& faces, int hueShift, double satScale) {
|
|
cv::Mat output = input.clone();
|
|
|
|
// Hue OpenCV 0..179, rotasi melingkar
|
|
cv::Mat hueLut(1, 256, CV_8U);
|
|
for (int i = 0; i < 256; ++i) {
|
|
hueLut.at<uchar>(i) = static_cast<uchar>(((i + hueShift) % 180 + 180) % 180);
|
|
}
|
|
|
|
for (const cv::Rect& face : faces) {
|
|
cv::Rect box = face & cv::Rect(0, 0, input.cols, input.rows);
|
|
if (box.empty()) continue;
|
|
cv::Mat roi = output(box);
|
|
|
|
cv::Mat ycrcb, skinMask;
|
|
cv::cvtColor(roi, ycrcb, cv::COLOR_BGR2YCrCb);
|
|
cv::inRange(ycrcb, cv::Scalar(0, 135, 80), cv::Scalar(255, 195, 135), skinMask);
|
|
|
|
cv::Mat ellipse = cv::Mat::zeros(roi.size(), CV_8U);
|
|
cv::ellipse(ellipse, cv::Point(roi.cols / 2, roi.rows / 2),
|
|
cv::Size(roi.cols / 2, roi.rows / 2), 0, 0, 360, cv::Scalar(255), cv::FILLED);
|
|
cv::bitwise_and(skinMask, ellipse, skinMask);
|
|
|
|
cv::Mat alpha;
|
|
skinMask.convertTo(alpha, CV_32F, 1.0 / 255.0);
|
|
cv::GaussianBlur(alpha, alpha, cv::Size(15, 15), 0);
|
|
|
|
cv::Mat hsv;
|
|
cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV);
|
|
std::vector<cv::Mat> ch;
|
|
cv::split(hsv, ch);
|
|
cv::LUT(ch[0], hueLut, ch[0]);
|
|
ch[1] = ch[1] * satScale;
|
|
cv::merge(ch, hsv);
|
|
|
|
cv::Mat recolored;
|
|
cv::cvtColor(hsv, recolored, cv::COLOR_HSV2BGR);
|
|
|
|
cv::Mat alpha3, roiF, recF, blended;
|
|
cv::merge(std::vector<cv::Mat>{alpha, alpha, alpha}, alpha3);
|
|
roi.convertTo(roiF, CV_32F);
|
|
recolored.convertTo(recF, CV_32F);
|
|
blended = recF.mul(alpha3) + roiF.mul(cv::Scalar::all(1.0) - alpha3);
|
|
blended.convertTo(roi, CV_8U);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
} // namespace ColorProcessing
|
|
|
|
namespace {
|
|
|
|
const int kHueShift = 60;
|
|
const double kSatScale = 1.3;
|
|
|
|
bool loadCascade(cv::CascadeClassifier& cascade, const std::string& userPath) {
|
|
if (!userPath.empty() && cascade.load(userPath)) return true;
|
|
if (cascade.load("haarcascade_frontalface_default.xml")) return true;
|
|
|
|
std::string found = cv::samples::findFile("haarcascades/haarcascade_frontalface_default.xml", false, false);
|
|
return !found.empty() && cascade.load(found);
|
|
}
|
|
|
|
int runWebcam(cv::CascadeClassifier& cascade, const std::string& outDir) {
|
|
cv::VideoCapture cap(0);
|
|
if (!cap.isOpened()) {
|
|
std::cerr << "Kesalahan: Webcam tidak dapat dibuka\n";
|
|
return 1;
|
|
}
|
|
|
|
std::cout << "[INFO] Webcam aktif. Tekan 's' simpan frame, 'q' atau Esc keluar.\n";
|
|
cv::Mat frame;
|
|
int saved = 0;
|
|
while (cap.read(frame)) {
|
|
auto faces = ColorProcessing::detectFaces(frame, cascade);
|
|
cv::Mat view = ColorProcessing::recolorFace(frame, faces, kHueShift, kSatScale);
|
|
cv::imshow("Face Recolor", view);
|
|
|
|
int key = cv::waitKey(1) & 0xFF;
|
|
if (key == 'q' || key == 27) break;
|
|
if (key == 's') {
|
|
std::string name = outDir + "/webcam_" + std::to_string(saved++) + ".png";
|
|
cv::imwrite(name, view);
|
|
std::cout << "[+] Tersimpan: " << name << "\n";
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char** argv) {
|
|
std::string inputPath, cascadePath;
|
|
bool useCam = (argc < 2);
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string arg = argv[i];
|
|
if (arg == "--cam") useCam = true;
|
|
else if (arg == "--cascade" && i + 1 < argc) cascadePath = argv[++i];
|
|
else inputPath = arg;
|
|
}
|
|
if (inputPath.empty()) useCam = true;
|
|
|
|
std::string outDir = "output_results";
|
|
fs::create_directories(outDir);
|
|
|
|
cv::CascadeClassifier cascade;
|
|
if (!loadCascade(cascade, cascadePath)) {
|
|
std::cerr << "Kesalahan: haarcascade_frontalface_default.xml tidak ditemukan. "
|
|
<< "Gunakan --cascade <path_xml>\n";
|
|
return 1;
|
|
}
|
|
|
|
if (useCam) return runWebcam(cascade, outDir);
|
|
|
|
cv::Mat src = cv::imread(inputPath, cv::IMREAD_COLOR);
|
|
|
|
if (src.empty()) {
|
|
std::cerr << "Kesalahan: Tidak dapat memuat gambar dari " << inputPath << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
fs::path p(inputPath);
|
|
std::string stem = p.stem().string();
|
|
|
|
std::cout << "[INFO] Memproses citra: " << inputPath << " (" << src.cols << "x" << src.rows << " px)\n";
|
|
|
|
// 1. Eksekusi Complement
|
|
cv::Mat imgComp = ColorProcessing::computeComplement(src);
|
|
cv::imwrite(outDir + "/" + stem + "_complement.png", imgComp);
|
|
std::cout << "[+] Selesai: Color Complement\n";
|
|
|
|
// 2. Eksekusi Tone S-Curve
|
|
cv::Mat imgTone = ColorProcessing::adjustToneSCurve(src);
|
|
cv::imwrite(outDir + "/" + stem + "_tone_scurve.png", imgTone);
|
|
std::cout << "[+] Selesai: Tonal S-Curve Enhancement\n";
|
|
|
|
// 3. Eksekusi HSI Equalization
|
|
cv::Mat imgHist = ColorProcessing::equalizeIntensityHSI(src);
|
|
cv::imwrite(outDir + "/" + stem + "_hsi_equalized.png", imgHist);
|
|
std::cout << "[+] Selesai: HSI Intensity Histogram Equalization\n";
|
|
|
|
// 4. Eksekusi Color Slicing (BGR: B=110, G=140, R=190 -> Estimasi Nada Kulit / Warm Tone)
|
|
cv::Vec3b targetSkinTone(110, 140, 190);
|
|
double thresholdRadius = 65.0;
|
|
cv::Mat imgSliced = ColorProcessing::sliceColorSphere(src, targetSkinTone, thresholdRadius);
|
|
cv::imwrite(outDir + "/" + stem + "_color_sliced.png", imgSliced);
|
|
std::cout << "[+] Selesai: Color Slicing (Euclidean Sphere)\n";
|
|
|
|
// 5. Eksekusi Laplacian Sharpening
|
|
cv::Mat imgSharp = ColorProcessing::sharpenLaplacian(src);
|
|
cv::imwrite(outDir + "/" + stem + "_sharpened.png", imgSharp);
|
|
std::cout << "[+] Selesai: Laplacian Sharpening\n";
|
|
|
|
// 6. Eksekusi Pseudocolor
|
|
cv::Mat imgPseudo = ColorProcessing::generatePseudocolor(src);
|
|
cv::imwrite(outDir + "/" + stem + "_pseudocolor.png", imgPseudo);
|
|
std::cout << "[+] Selesai: Pseudocolor Transformation\n";
|
|
|
|
// 7. Deteksi wajah + ubah warna kulit
|
|
auto faces = ColorProcessing::detectFaces(src, cascade);
|
|
cv::Mat imgFace = ColorProcessing::recolorFace(src, faces, kHueShift, kSatScale);
|
|
cv::imwrite(outDir + "/" + stem + "_face_recolor.png", imgFace);
|
|
std::cout << "[+] Selesai: Face Recolor (" << faces.size() << " wajah terdeteksi)\n";
|
|
|
|
std::cout << "\nSemua proses selesai. Hasil tersimpan di folder: ./" << outDir << "/" << std::endl;
|
|
|
|
return 0;
|
|
} |