#include #include #include #include #include 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(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 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(r); cv::Vec3b* outRow = output.ptr(r); for (int c = 0; c < input.cols; ++c) { double db = static_cast(inRow[c][0]) - prototypeColor[0]; double dg = static_cast(inRow[c][1]) - prototypeColor[1]; double dr = static_cast(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_(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 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 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& 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(i) = static_cast(((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 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{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 \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; }