first commit

This commit is contained in:
MelvynFaith 2026-09-20 21:35:29 +07:00
commit 58d45e99be
14 changed files with 826 additions and 0 deletions

31
.gitignore vendored Normal file
View File

@ -0,0 +1,31 @@
# Build
build/
cmake-build-*/
CMakeCache.txt
CMakeFiles/
*.exe
*.dll
*.o
*.obj
# Hasil program
output_results/
# LaTeX (laporan)
report/*.aux
report/*.log
report/*.out
report/*.toc
report/*.lof
report/*.lot
report/*.fls
report/*.fdb_latexmk
report/*.synctex.gz
report/*.pdf
# Editor / OS
.vscode/
.idea/
.vs/
.DS_Store
Thumbs.db

17
CMakeLists.txt Normal file
View File

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.10)
project(ColorProcessor CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(OpenCV REQUIRED)
add_executable(color_processor color_processor.cpp)
target_link_libraries(color_processor ${OpenCV_LIBS})
# Haar cascade dan DLL OpenCV disalin ke folder exe
set(CASCADE_XML "${OpenCV_DIR}/../../../etc/haarcascades/haarcascade_frontalface_default.xml")
add_custom_command(TARGET color_processor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CASCADE_XML}" "$<TARGET_FILE_DIR:color_processor>"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${OpenCV_DIR}/../bin" "$<TARGET_FILE_DIR:color_processor>"
)

100
README.md Normal file
View File

@ -0,0 +1,100 @@
# Color Processor: Pemrosesan Citra Warna dan Pengubahan Warna Wajah
Tugas mata kuliah **Pengolahan Citra Digital** (Gonzalez & Woods, Bab 6: *Color Image Processing*), ditulis dengan C++17 dan OpenCV.
Program menerapkan enam operasi pemrosesan citra warna, lalu mendeteksi wajah dan mengubah warna kulitnya. Masukan dapat berupa berkas gambar atau kamera (*webcam*).
- **Nama:** Efraim Melvyn Rafelino Lahai
- **NRP:** 5803024009
- **Program Studi:** Informatika
- **Dosen Pengajar:** Ir. Drs. Peter Rhatodirdjo Angka, M.Kom., IPM., ASEAN Eng.
## Fitur
| No | Operasi | Fungsi |
|----|---------|--------|
| 1 | Komplemen warna | `computeComplement` |
| 2 | Kurva S (penyesuaian nada) | `adjustToneSCurve` |
| 3 | Ekualisasi histogram kanal *Value* (HSV) | `equalizeIntensityHSI` |
| 4 | Pengirisan warna berbentuk bola 3D | `sliceColorSphere` |
| 5 | Penajaman Laplacian | `sharpenLaplacian` |
| 6 | Pseudowarna (JET) | `generatePseudocolor` |
| 7 | Deteksi wajah (Haar cascade + CLAHE) | `detectFaces` |
| 8 | Ubah warna kulit wajah (rotasi *Hue* di HSV, masker kulit YCrCb) | `recolorFace` |
Pengubahan warna wajah memutar *Hue* sebesar 60 dan mengalikan *Saturation* dengan 1,3, sehingga kulit menjadi hijau. Nilai ini dapat diubah lewat `kHueShift` dan `kSatScale` di `color_processor.cpp`.
## Kebutuhan
- Kompilator C++17 (diuji dengan MinGW-w64 GCC 16.1)
- CMake 3.10 atau lebih baru
- OpenCV 4.x dengan modul `core`, `imgproc`, `imgcodecs`, `videoio`, `highgui`, `objdetect` (diuji dengan 4.10.0)
- Berkas `haarcascade_frontalface_default.xml` (ada di folder `etc/haarcascades` atau `data/haarcascades` pada instalasi OpenCV)
> Catatan: pada MinGW, OpenCV harus dikompilasi dengan kompilator yang sama. Pada pengujian, MSMF dimatikan (`-DWITH_MSMF=OFF`) karena gagal dikompilasi dengan MinGW, sehingga kamera memakai DirectShow.
## Kompilasi
```
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOpenCV_DIR=<folder OpenCVConfig.cmake>
cmake --build build
```
Contoh `OpenCV_DIR` pada Windows dengan MinGW: `D:/opencv/install/x64/mingw/lib`.
Setelah *build*, CMake menyalin `haarcascade_frontalface_default.xml` dan DLL OpenCV ke folder `build/`. Jika berkas XML tidak ditemukan, program menampilkan pesan kesalahan; gunakan opsi `--cascade`.
## Penggunaan
Jalankan dari dalam folder `build`, karena program mencari berkas XML di folder kerja.
```
# Mode berkas: jalankan semua operasi pada satu gambar
.\color_processor.exe path\ke\foto.jpg
# Mode kamera
.\color_processor.exe --cam
# Tentukan lokasi berkas cascade sendiri
.\color_processor.exe foto.jpg --cascade path\ke\haarcascade_frontalface_default.xml
```
Tanpa argumen, program membuka kamera.
**Mode berkas** menyimpan hasil ke `output_results/` dengan format PNG:
```
<nama>_complement.png
<nama>_tone_scurve.png
<nama>_hsi_equalized.png
<nama>_color_sliced.png
<nama>_sharpened.png
<nama>_pseudocolor.png
<nama>_face_recolor.png
```
**Mode kamera:** tekan `s` untuk menyimpan bingkai (`output_results/webcam_N.png`), `q` atau `Esc` untuk keluar.
## Struktur Proyek
```
.
├── color_processor.cpp # kode program
├── CMakeLists.txt
├── report/ # laporan LaTeX dan gambar hasil
└── README.md
```
## Keterbatasan
- Haar cascade bergantung pada pencahayaan. Pada ruangan redup, wajah dapat gagal terdeteksi.
- Wajah yang menunduk, miring, atau tertutup (misalnya topi) sulit dideteksi.
- Menurunkan ambang deteksi menambah kemungkinan positif palsu.
- Rentang warna kulit di YCrCb bersifat tetap, sehingga objek berwarna serupa di dalam kotak wajah (misalnya tepi topi krem) dapat ikut berubah.
- Pengirisan warna memakai satu warna prototipe tetap, sehingga hanya cocok untuk satu nada kulit.
- Mode kamera belum diuji ulang pada kondisi redup setelah penggantian ke CLAHE.
## Referensi
- R. C. Gonzalez dan R. E. Woods, *Digital Image Processing*, Bab 6.
- OpenCV: <https://opencv.org>

293
color_processor.cpp Normal file
View File

@ -0,0 +1,293 @@
#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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

385
report/laporan.tex Normal file
View File

@ -0,0 +1,385 @@
\documentclass[12pt,a4paper]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage[indonesian]{babel}
\usepackage{lmodern}
\usepackage[margin=2.5cm]{geometry}
\usepackage{graphicx}
\usepackage{caption}
\usepackage{subcaption}
\usepackage{amsmath}
\usepackage{listings}
\usepackage{xcolor}
\usepackage{booktabs}
\usepackage{float}
\usepackage{setspace}
\usepackage[hidelinks]{hyperref}
\graphicspath{{images/}}
\setstretch{1.15}
\lstset{
language=C++,
basicstyle=\ttfamily\footnotesize,
keywordstyle=\color{blue!60!black},
commentstyle=\color{green!40!black},
stringstyle=\color{red!60!black},
numbers=left,
numberstyle=\tiny\color{gray},
breaklines=true,
frame=single,
tabsize=4,
showstringspaces=false
}
\begin{document}
% ---------------- Halaman judul ----------------
\begin{titlepage}
\centering
\vspace*{2cm}
{\Large \textbf{LAPORAN TUGAS}}\\[0.6cm]
{\Large \textbf{PENGOLAHAN CITRA DIGITAL}}\\[1.5cm]
{\LARGE \textbf{Pemrosesan Citra Warna dan Pengubahan Warna Wajah Menggunakan C++ dan OpenCV}}\\[0.6cm]
{\large (Gonzalez \& Woods, Bab 6: Color Image Processing)}\\[3cm]
\begin{tabular}{ll}
\textbf{Nama} & : Efraim Melvyn Rafelino Lahai \\
\textbf{NRP} & : 5803024009 \\
\textbf{Program Studi} & : Informatika \\
\textbf{Mata Kuliah} & : Pengolahan Citra Digital \\
\textbf{Dosen Pengajar} & : Ir. Drs. Peter Rhatodirdjo Angka, M.Kom., IPM., ASEAN Eng.
\end{tabular}
\vfill
{\large \today}
\end{titlepage}
\tableofcontents
\newpage
% ---------------- Bab 1 ----------------
\section{Pendahuluan}
\subsection{Latar Belakang}
Citra berwarna membawa informasi yang jauh lebih kaya dibandingkan citra
keabuan. Bab 6 buku \textit{Digital Image Processing} (Gonzalez \& Woods)
membahas model warna, transformasi warna, penghalusan dan penajaman citra
berwarna, serta segmentasi berbasis warna. Tugas ini menerapkan konsep-konsep
tersebut dalam sebuah program C++ yang memproses citra wajah, baik dari berkas
gambar maupun dari kamera (\textit{webcam}) secara langsung.
\subsection{Tujuan}
\begin{enumerate}
\item Mengimplementasikan beberapa operasi pemrosesan citra warna dari Bab 6.
\item Mendeteksi wajah pada citra atau video, lalu mengubah warna kulit wajah
secara otomatis.
\item Menganalisis hasil dan keterbatasan program.
\end{enumerate}
\subsection{Batasan}
Program menggunakan pustaka OpenCV untuk pembacaan citra, konversi ruang warna,
dan deteksi wajah. Beberapa operasi dasar (misalnya pengirisan warna) ditulis
manual per piksel, sedangkan operasi lain memakai fungsi bawaan OpenCV.
% ---------------- Bab 2 ----------------
\section{Landasan Teori}
\subsection{Komplemen Warna}
Komplemen warna pada citra 8-bit (\(L = 256\)) dihitung per komponen:
\begin{equation}
s_i = (L - 1) - r_i, \qquad i = B, G, R.
\end{equation}
Operasi ini setara dengan negatif citra dan diterapkan pada setiap kanal.
\subsection{Penyesuaian Nada dengan Kurva S}
Kurva S meningkatkan kontras dengan menggelapkan nada gelap dan menerangkan
nada terang. Program memakai fungsi sigmoid yang dinormalisasi agar
\(f(0)=0\) dan \(f(255)=255\):
\begin{equation}
\sigma(r) = \frac{1}{1 + e^{-k(r-128)}}, \qquad
f(r) = 255\,\frac{\sigma(r) - \sigma(0)}{\sigma(255) - \sigma(0)}, \qquad k = 0{,}035.
\end{equation}
Fungsi ini dihitung sekali dalam bentuk tabel pencarian (\textit{look-up table})
berisi 256 nilai, lalu diterapkan ke seluruh piksel.
\subsection{Ekualisasi Histogram pada Kanal Intensitas}
Ekualisasi histogram langsung pada kanal R, G, dan B akan menggeser rona
sehingga muncul warna palsu. Cara yang lebih aman adalah mengubah citra ke ruang
warna yang memisahkan informasi warna dari kecerahan, lalu meratakan histogram
hanya pada komponen kecerahan. Program memakai ruang HSV dan meratakan kanal
\textit{Value}, sebagai pendekatan terhadap kanal intensitas pada model HSI.
Kanal \textit{Saturation} dinaikkan 15\%.
\subsection{Pengirisan Warna (\textit{Color Slicing}) Berbentuk Bola}
Pengirisan warna mengisolasi warna tertentu. Untuk daerah berbentuk bola
dengan pusat \(\mathbf{a} = (a_B, a_G, a_R)\) dan jari-jari \(R_0\), piksel
\(\mathbf{z}\) dipertahankan jika jaraknya ke pusat tidak melebihi \(R_0\), dan
diganti dengan warna netral jika sebaliknya:
\begin{equation}
s_i =
\begin{cases}
128 & \text{jika } \sum_{j}(z_j - a_j)^2 > R_0^2, \\
r_i & \text{lainnya.}
\end{cases}
\end{equation}
Nilai 128 adalah padanan 8-bit dari abu-abu netral \(0{,}5\).
\subsection{Penajaman Citra Warna dengan Laplacian}
Laplacian vektor pada citra warna sama dengan Laplacian yang dihitung terpisah
pada tiap komponen warna. Penajaman dilakukan dengan mengurangkan Laplacian
dari citra asli, \(g = f - \nabla^2 f\). Untuk Laplacian 4-tetangga, hal ini
menghasilkan kernel
\begin{equation}
\begin{bmatrix}
0 & -1 & 0 \\
-1 & 5 & -1 \\
0 & -1 & 0
\end{bmatrix}.
\end{equation}
\subsection{Pemrosesan Pseudowarna}
Pseudowarna menugaskan warna pada tiap tingkat keabuan sehingga perbedaan
intensitas yang halus lebih mudah dilihat. Program mengubah citra ke skala
keabuan lalu menerapkan peta warna JET.
\subsection{Deteksi Wajah dengan Haar Cascade}
Deteksi wajah memakai \textit{Haar cascade classifier} (metode Viola--Jones)
yang tersedia di OpenCV, berkas \texttt{haarcascade\_frontalface\_default.xml}.
Pengklasifikasi ini memindai citra keabuan pada beberapa skala dan mengembalikan
kotak pembatas untuk wajah menghadap depan.
\subsection{Segmentasi Kulit di Ruang YCrCb}
Warna kulit manusia mengelompok pada rentang sempit di komponen krominansi
\(C_r\) dan \(C_b\) dan relatif tidak bergantung pada kecerahan (\(Y\)).
Karena itu piksel kulit dapat dipisahkan dengan ambang sederhana pada
\(C_r\) dan \(C_b\).
% ---------------- Bab 3 ----------------
\section{Perancangan dan Implementasi}
\subsection{Lingkungan Pengembangan}
\begin{itemize}
\item Bahasa: C++17 (kompilator MinGW-w64 GCC 16.1, UCRT).
\item Pustaka: OpenCV 4.10.0, dikompilasi dari kode sumber dengan modul
\texttt{core}, \texttt{imgproc}, \texttt{imgcodecs}, \texttt{videoio},
\texttt{highgui}, dan \texttt{objdetect}.
\item Sistem \textit{build}: CMake dan Ninja.
\item Sistem operasi: Windows 11.
\end{itemize}
\subsection{Alur Program}
\begin{enumerate}
\item Program dijalankan dengan jalur berkas gambar, atau dengan
\texttt{--cam} (atau tanpa argumen) untuk mode kamera.
\item Berkas \textit{Haar cascade} dimuat.
\item \textbf{Mode berkas:} enam operasi Bab 6 dijalankan pada gambar
masukan, lalu wajah dideteksi dan warnanya diubah. Semua hasil disimpan
dalam format PNG ke folder \texttt{output\_results}.
\item \textbf{Mode kamera:} setiap bingkai dideteksi wajahnya dan diubah
warnanya, lalu ditampilkan. Tombol \texttt{s} menyimpan bingkai,
tombol \texttt{q} atau \texttt{Esc} keluar.
\end{enumerate}
\subsection{Fungsi Utama}
\begin{table}[H]
\centering
\caption{Fungsi pada namespace \texttt{ColorProcessing}.}
\begin{tabular}{@{}lp{9cm}@{}}
\toprule
Fungsi & Keterangan \\
\midrule
\texttt{computeComplement} & Komplemen warna dengan \texttt{bitwise\_not}. \\
\texttt{adjustToneSCurve} & Kurva S berbasis tabel pencarian. \\
\texttt{equalizeIntensityHSI} & Ekualisasi kanal \textit{Value} di HSV. \\
\texttt{sliceColorSphere} & Pengirisan warna bola 3D, ditulis manual per piksel. \\
\texttt{sharpenLaplacian} & Penajaman dengan kernel Laplacian 3$\times$3. \\
\texttt{generatePseudocolor} & Keabuan lalu peta warna JET. \\
\texttt{detectFaces} & Deteksi wajah dengan Haar cascade. \\
\texttt{recolorFace} & Pengubahan warna kulit di dalam wajah. \\
\bottomrule
\end{tabular}
\end{table}
\subsection{Pengubahan Warna Wajah}
Fungsi \texttt{recolorFace} bekerja pada setiap kotak wajah hasil deteksi:
\begin{enumerate}
\item Bagian wajah diubah ke ruang YCrCb, lalu piksel kulit dipilih dengan
ambang \(C_r \in [135, 195]\) dan \(C_b \in [80, 135]\).
\item Hasilnya dikalikan (operasi AND) dengan elips yang mengisi kotak wajah,
sehingga latar di sudut kotak tidak ikut berubah.
\item Masker dihaluskan dengan \textit{Gaussian blur} 15$\times$15 agar tepi
transisinya lembut.
\item Bagian wajah diubah ke HSV. Kanal \textit{Hue} diputar sebesar 60
(skala OpenCV 0--179, sehingga kulit oranye menjadi hijau) dan
\textit{Saturation} dikalikan 1{,}3.
\item Hasil putaran warna dicampur dengan citra asli menurut masker:
\(\text{keluaran} = \alpha \cdot \text{berubah} + (1-\alpha) \cdot \text{asli}\).
\end{enumerate}
Karena hanya piksel kulit di dalam wajah yang bobotnya besar, rambut, mata,
dan latar tetap berwarna asli.
\subsection{Peningkatan Ketahanan Deteksi terhadap Cahaya}
Pengujian awal menunjukkan deteksi wajah gagal pada citra dengan pencahayaan
kurang merata. Ekualisasi histogram global (\texttt{equalizeHist}) diganti
dengan CLAHE (\textit{Contrast Limited Adaptive Histogram Equalization}, batas
kontras 3,0, petak 8$\times$8) sebelum deteksi. Setelah perubahan ini, foto uji
yang sebelumnya menghasilkan 0 wajah kini menghasilkan 1 wajah.
% ---------------- Bab 4 ----------------
\section{Hasil dan Pembahasan}
Pengujian memakai foto penulis (Gambar~\ref{fig:asli}) dan citra uji standar
\textit{Lena} yang disertakan dalam kode sumber OpenCV.
\begin{figure}[H]
\centering
\includegraphics[width=0.4\textwidth]{foto_asli.jpeg}
\caption{Citra masukan (foto penulis).}
\label{fig:asli}
\end{figure}
\subsection{Hasil Operasi Bab 6}
\begin{figure}[H]
\centering
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_complement.png}
\caption{Komplemen warna}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_tone_scurve.png}
\caption{Kurva S}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_hsi_equalized.png}
\caption{Ekualisasi kanal $V$}
\end{subfigure}
\caption{Transformasi warna dan nada.}
\label{fig:transformasi}
\end{figure}
\begin{figure}[H]
\centering
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_color_sliced.png}
\caption{Pengirisan warna}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_sharpened.png}
\caption{Penajaman Laplacian}
\end{subfigure}
\hfill
\begin{subfigure}[b]{0.32\textwidth}
\includegraphics[width=\textwidth]{foto_pseudocolor.png}
\caption{Pseudowarna JET}
\end{subfigure}
\caption{Pengirisan warna, penajaman, dan pseudowarna.}
\label{fig:lanjutan}
\end{figure}
\textbf{Komplemen} membalik seluruh warna: kulit menjadi kebiruan dan putih
menjadi gelap. \textbf{Kurva S} menambah kontras nada tengah. \textbf{Ekualisasi
kanal \textit{Value}} meratakan sebaran kecerahan tanpa menggeser rona.
\textbf{Penajaman Laplacian} mempertegas tepi dan tekstur. \textbf{Pseudowarna}
mengubah tingkat keabuan menjadi warna sehingga daerah terang dan gelap lebih
mudah dibedakan.
Pada \textbf{pengirisan warna}, hanya sebagian kulit wajah yang dipertahankan,
sedangkan sisanya menjadi abu-abu. Hal ini terjadi karena pusat bola
(BGR = 110, 140, 190) dan jari-jari 65 bersifat tetap, sehingga bagian wajah
yang lebih terang atau lebih gelap karena bayangan jatuh di luar bola.
Beberapa objek berwarna hangat di latar (misalnya dinding krem) juga ikut
lolos. Ini menunjukkan bahwa pengirisan warna dengan prototipe tetap peka
terhadap pencahayaan dan warna kulit.
\subsection{Hasil Pengubahan Warna Wajah}
\begin{figure}[H]
\centering
\begin{subfigure}[b]{0.4\textwidth}
\includegraphics[width=\textwidth]{foto_asli.jpeg}
\caption{Sebelum}
\end{subfigure}
\hspace{0.05\textwidth}
\begin{subfigure}[b]{0.4\textwidth}
\includegraphics[width=\textwidth]{foto_face_recolor.png}
\caption{Sesudah}
\end{subfigure}
\caption{Pengubahan warna wajah pada foto penulis (1 wajah terdeteksi).}
\label{fig:recolor}
\end{figure}
Wajah terdeteksi dan kulit berubah menjadi hijau, sedangkan baju, kursi, dan
latar tidak berubah. Pada tepi topi terlihat sedikit rona hijau karena
warna topi krem masuk ke dalam rentang kulit dan berada di dalam kotak wajah.
\begin{figure}[H]
\centering
\includegraphics[width=0.45\textwidth]{lena_face_recolor.png}
\caption{Pengubahan warna wajah pada citra uji \textit{Lena}.}
\label{fig:lena}
\end{figure}
Pada citra \textit{Lena}, wajah utama berubah warna dengan baik. Dengan
parameter deteksi yang sekarang, program mengembalikan 2 kotak wajah pada
citra ini, sehingga ada satu deteksi tambahan (positif palsu).
\subsection{Pengujian Kamera}
Mode kamera bekerja dan menampilkan wajah dengan warna yang sudah diubah secara
langsung. Namun pada ruangan yang kurang terang, wajah awalnya tidak
terdeteksi; deteksi baru berhasil setelah wajah diberi cahaya tambahan dari
senter ponsel. Pengujian kamera setelah penggantian ke CLAHE belum
dilakukan pada kondisi redup yang sama.
\subsection{Keterbatasan}
\begin{enumerate}
\item \textbf{Bergantung pada pencahayaan.} Haar cascade dilatih pada wajah
menghadap depan dengan pencahayaan cukup. Cahaya redup, bayangan kuat,
wajah menunduk atau miring, serta penutup seperti topi dapat
menggagalkan deteksi.
\item \textbf{Positif palsu.} Menurunkan ambang deteksi meningkatkan
kemungkinan objek non-wajah terdeteksi sebagai wajah.
\item \textbf{Ambang kulit tetap.} Rentang \(C_r/C_b\) yang tetap tidak
cocok untuk semua warna kulit dan pencahayaan, dan dapat mengenai
objek berwarna serupa (topi, kayu, dinding).
\item \textbf{Pengirisan warna} memakai prototipe tetap, sehingga hasilnya
hanya baik untuk satu nada kulit.
\item \textbf{Bukan pelacakan.} Setiap bingkai dideteksi ulang tanpa
pelacakan, sehingga kotak dapat berkedip antarbingkai.
\end{enumerate}
\subsection{Saran Pengembangan}
Detektor wajah berbasis jaringan saraf (misalnya model DNN pada OpenCV) lebih
tahan terhadap cahaya dan sudut pandang dibandingkan Haar cascade. Ambang kulit
juga dapat dibuat adaptif, misalnya dengan mengambil statistik warna dari
bagian tengah wajah yang terdeteksi.
% ---------------- Bab 5 ----------------
\section{Kesimpulan}
Program berhasil menerapkan enam operasi pemrosesan citra warna dari Bab 6
(komplemen, kurva S, ekualisasi kanal intensitas, pengirisan warna,
penajaman Laplacian, dan pseudowarna) serta mendeteksi wajah dan mengubah warna
kulitnya pada gambar dan pada kamera. Pengubahan warna paling baik pada wajah
menghadap depan dengan pencahayaan cukup. Penggantian ekualisasi global dengan
CLAHE meningkatkan deteksi pada foto uji, tetapi deteksi pada cahaya redup
dan positif palsu masih menjadi keterbatasan.
% ---------------- Lampiran ----------------
\appendix
\section{Cara Kompilasi dan Menjalankan}
\begin{lstlisting}[language={},numbers=none]
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release ^
-DOpenCV_DIR=D:/opencv/install/x64/mingw/lib
cmake --build build
cd build
.\color_processor.exe path\ke\foto.jpg # mode berkas
.\color_processor.exe --cam # mode kamera
\end{lstlisting}
\end{document}