From 7e5180ecffe21a2c18f54fa33f553c829f616e46 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 8 May 2026 21:15:09 +0900 Subject: [PATCH 01/48] =?UTF-8?q?=E2=9C=A8=20Create=20perturbation=5Fexpla?= =?UTF-8?q?iner.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._explainers.py => eigenvalue_explainer.py} | 0 explainability/perturbation_explainer.py | 38 +++++++++++++++++++ 2 files changed, 38 insertions(+) rename explainability/{eigenvalue_explainers.py => eigenvalue_explainer.py} (100%) create mode 100644 explainability/perturbation_explainer.py diff --git a/explainability/eigenvalue_explainers.py b/explainability/eigenvalue_explainer.py similarity index 100% rename from explainability/eigenvalue_explainers.py rename to explainability/eigenvalue_explainer.py diff --git a/explainability/perturbation_explainer.py b/explainability/perturbation_explainer.py new file mode 100644 index 0000000..8a451e4 --- /dev/null +++ b/explainability/perturbation_explainer.py @@ -0,0 +1,38 @@ +from pytorch_grad_cam import AblationCAM, ScoreCAM, FEM +from explainability.cam_explainer import CAMExplainer + + +class AblationCAMExplainer(CAMExplainer): + """ + Zero out activations and measure how the output drops + (this repository includes a fast batched implementation) + """ + def __init__(self, batch_size: int = 32, ratio_channels_to_ablate: float = 1.0, **kwargs): + self.batch_size = batch_size + self.ratio_channels_to_ablate = ratio_channels_to_ablate + super().__init__(**kwargs) + + def _build_cam(self): + return AblationCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, + batch_size=self.batch_size, ratio_channels_to_ablate=self.ratio_channels_to_ablate) + + +class ScoreCAMExplainer(CAMExplainer): + """ + Perbutate the image by the scaled activation + mesaure how the output drops + """ + def _build_cam(self): + return ScoreCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) + +class FEMExplainer(CAMExplainer): + """ + A gradient free method that binarizes activations + by an activation > mean + k * std rule + """ + def __init__(self, k: int = 2, **kwargs): + self.k = k + super().__init__(**kwargs) + + def _build_cam(self): + return FEM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, k=self.k) \ No newline at end of file From 102ef5743c6a98d2063206725f7220dd88adf7c2 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 8 May 2026 21:18:27 +0900 Subject: [PATCH 02/48] =?UTF-8?q?=E2=9C=A8=20Create=20misc=5Fexplainer.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/misc_explainer.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 explainability/misc_explainer.py diff --git a/explainability/misc_explainer.py b/explainability/misc_explainer.py new file mode 100644 index 0000000..db6afa0 --- /dev/null +++ b/explainability/misc_explainer.py @@ -0,0 +1,13 @@ +from pytorch_grad_cam import LayerCAM, RandomCAM +from explainability.cam_explainer import CAMExplainer + + +class LayerCAMExplainer(CAMExplainer): + """ + Spatially weight the activations by positive gradients. + Works better especially in lower layers + """ + def _build_cam(self): + return LayerCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) + + From 065a228b2947a6c282285894298e9155e44162b1 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 8 May 2026 21:25:39 +0900 Subject: [PATCH 03/48] =?UTF-8?q?=F0=9F=93=9D=20Add=20=5F=5Frepr=5F=5F=20f?= =?UTF-8?q?or=20all=20explainer=20classes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/base_explainer.py | 8 +++++-- explainability/cam_explainer.py | 12 ++++++++-- explainability/eigenvalue_explainer.py | 3 +++ explainability/gradient_explainer.py | 29 ++---------------------- explainability/perturbation_explainer.py | 7 +++++- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/explainability/base_explainer.py b/explainability/base_explainer.py index 1a54186..951c6f1 100644 --- a/explainability/base_explainer.py +++ b/explainability/base_explainer.py @@ -26,8 +26,10 @@ def __init__( self.device = "cuda:0" if torch.cuda.is_available() else 'cpu' self.margin_ratio = margin_ratio + self.conf_thres = conf_thres self.category = category self.model_name = model_name + self.dataset = dataset self.branch_level = branch_level self.l_stage_idx = l_stage_idx @@ -109,5 +111,7 @@ def _get_transform(self, img_path: str): def __repr__(self): return (f"{self.__class__.__name__}(" - f"model={self.model_name}, branch={self.branch}, " - f"device={self.device})") \ No newline at end of file + f"model={self.model_name}, dataset={self.dataset}, " + f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " + f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, " + f"category={self.category},device={self.device})") \ No newline at end of file diff --git a/explainability/cam_explainer.py b/explainability/cam_explainer.py index aa5fa96..6254366 100644 --- a/explainability/cam_explainer.py +++ b/explainability/cam_explainer.py @@ -44,7 +44,7 @@ def display_cam_on_image(self, img_path): deprocess_image(tensor), # (H,W,3), range(0~1), np.ndarray grayscale_cam, # (H,W), range(0~1), np.ndarray use_rgb=True, - colormap = self.colormap + colormap = self.colormap, image_weight=self.image_weight) return heatmap # (H,W,3), range(0~255), np.uint8 @@ -53,5 +53,13 @@ def display_inverse_cam(self, img_path): grayscale_cam, _ = self.display_heatmap(img_path) return 1 - grayscale_cam # (H,W), range(0~1), np.float32 - + + def __repr__(self): + return (f"{self.__class__.__name__}(" + f"model={self.model_name}, dataset={self.dataset}, " + f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " + f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, " + f"aug_smooth={self.aug_smooth}, eigen_smooth={self.eigen_smooth}, " + f"colormap={self.colormap}, image_weight={self.image_weight}, " + f"category={self.category},device={self.device})") \ No newline at end of file diff --git a/explainability/eigenvalue_explainer.py b/explainability/eigenvalue_explainer.py index 7f53174..5eb95c7 100644 --- a/explainability/eigenvalue_explainer.py +++ b/explainability/eigenvalue_explainer.py @@ -31,6 +31,9 @@ def _build_cam(self): return KPCA_CAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, kernel=self.kernel, gamma=self.gamma) + def __repr__(self): + return super().__repr__().rstrip(")") + f", kernel={self.kernel}, gamma={self.gamma})" + class SegEigenCAMExplainer(CAMExplainer): """ Like EigenCAM but with gradient weighting (absolute gradients ⊙ activations ) diff --git a/explainability/gradient_explainer.py b/explainability/gradient_explainer.py index f50bff9..0282fd6 100644 --- a/explainability/gradient_explainer.py +++ b/explainability/gradient_explainer.py @@ -1,6 +1,6 @@ from pytorch_grad_cam import ( GradCAM, GradCAMPlusPlus, GradCAMElementWise, - XGradCAM, HiResCAM, FullGrad, ShapleyCAM, FinerCAM, + XGradCAM, HiResCAM ) from explainability.cam_explainer import CAMExplainer @@ -44,29 +44,4 @@ def _build_cam(self): return HiResCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) -class FullGradExplainer(CAMExplainer): - """ - Computes the gradients of the biases from all over the network, and then sums them - """ - def _build_cam(self): - return FullGrad(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) - - -class ShapleyCAMExplainer(CAMExplainer): - """ - Weight the activations using the gradient and Hessian-vector product. - """ - def _build_cam(self): - return ShapleyCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) - - -class FinerCAMExplainer(CAMExplainer): - """ - Improves fine-grained classification by comparing similar classes, suppressing shared features and highlighting discriminative details. - """ - def __init__(self, base_method=GradCAM, **kwargs): - self.base_method = base_method - super().__init__(**kwargs) - - def _build_cam(self): - return FinerCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, base_method=self.base_method) \ No newline at end of file + diff --git a/explainability/perturbation_explainer.py b/explainability/perturbation_explainer.py index 8a451e4..28730ed 100644 --- a/explainability/perturbation_explainer.py +++ b/explainability/perturbation_explainer.py @@ -15,6 +15,8 @@ def __init__(self, batch_size: int = 32, ratio_channels_to_ablate: float = 1.0, def _build_cam(self): return AblationCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, batch_size=self.batch_size, ratio_channels_to_ablate=self.ratio_channels_to_ablate) + def __repr__(self): + return super().__repr__().rstrip(")") + f", ratio={self.ratio_channels_to_ablate})" class ScoreCAMExplainer(CAMExplainer): @@ -35,4 +37,7 @@ def __init__(self, k: int = 2, **kwargs): super().__init__(**kwargs) def _build_cam(self): - return FEM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, k=self.k) \ No newline at end of file + return FEM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, k=self.k) + + def __repr__(self): + return super().__repr__().rstrip(")") + f", k={self.k})" \ No newline at end of file From 857b308b1aa6ee5566492312177cff7496874502 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 16:03:55 +0900 Subject: [PATCH 04/48] =?UTF-8?q?=E2=9C=A8=20Add=20=5Fresolve=5Fblock=5Fid?= =?UTF-8?q?x?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/base_explainer.py | 42 +++++++++++++++++++++++--------- explainability/cam_explainer.py | 2 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/explainability/base_explainer.py b/explainability/base_explainer.py index 951c6f1..6054772 100644 --- a/explainability/base_explainer.py +++ b/explainability/base_explainer.py @@ -20,7 +20,8 @@ def __init__( conf_thres: float = 0.5, category: int = 1, # 0: real | 1: fake branch_level: str = "both", # low | high | both - l_stage_idx: int = -1 # gcvit low branch stage index (0~3) + l_stage_idx: int = -1, # gcvit low branch stage index (0~3) + block_idx: int = -1 # vit, gcvit block index ): self._validate_inputs(model_name, dataset, branch_level) @@ -32,6 +33,7 @@ def __init__( self.dataset = dataset self.branch_level = branch_level self.l_stage_idx = l_stage_idx + self.block_idx = block_idx # Parse Model Metadata self.model_variant = model_name.split("_")[-1] # b0, b5 @@ -60,27 +62,45 @@ def _validate_inputs(self, model_name: str, dataset: str, branch_level: str): def _get_reshape_fn(self): return reshape_transform_vit if self.transformer_type == "vit" else reshape_transform_gcvit - def _resolve_target_layers(self) -> list[torch.nn.Module]: + def _resolve_target_layers(self) -> list: layers = [] if self.transformer_type == "gcvit": - n_stages = len(self.model.l_gcvit.stages) - idx = self.l_stage_idx if self.l_stage_idx >= 0 else n_stages + self.l_stage_idx + n_stages = len(self.model.l_gcvit.stages) + stage_idx = self.l_stage_idx if self.l_stage_idx >= 0 else n_stages + self.l_stage_idx + + if not (0 <= stage_idx < n_stages): + raise ValueError(f"l_stage_idx={self.l_stage_idx} 범위 초과 (stage 수: {n_stages})") - if not (0 <= idx < n_stages): - raise IndexError(f"l_stage_idx {self.l_stage_idx} out of range (n={n_stages})") if self.branch_level in ("low", "both"): - layers.append(self.model.l_gcvit.stages[idx].blocks[-1].norm2) + n_blocks = len(self.model.l_gcvit.stages[stage_idx].blocks) + blk = self._resolve_block_idx(self.block_idx, n_blocks, f"l_gcvit stage[{stage_idx}]") + layers.append(self.model.l_gcvit.stages[stage_idx].blocks[blk].norm2) + if self.branch_level in ("high", "both"): - layers.append(self.model.h_gcvit.stages[0].blocks[-1].norm2) + n_blocks = len(self.model.h_gcvit.stages[0].blocks) + blk = self._resolve_block_idx(self.block_idx, n_blocks, "h_gcvit stage[0]") + layers.append(self.model.h_gcvit.stages[0].blocks[blk].norm2) else: # vit if self.branch_level in ("low", "both"): - layers.append(self.model.l_vit.blocks[-1].norm1) + n_blocks = len(self.model.l_vit.blocks) + blk = self._resolve_block_idx(self.block_idx, n_blocks, "l_vit") + layers.append(self.model.l_vit.blocks[blk].norm1) + if self.branch_level in ("high", "both"): - layers.append(self.model.h_vit.blocks[-1].norm1) + n_blocks = len(self.model.h_vit.blocks) + blk = self._resolve_block_idx(self.block_idx, n_blocks, "h_vit") + layers.append(self.model.h_vit.blocks[blk].norm1) return layers + + @staticmethod + def _resolve_block_idx(block_idx: int, n_blocks: int, name: str) -> int: + idx = block_idx if block_idx >= 0 else n_blocks + block_idx + if not (0 <= idx < n_blocks): + raise ValueError(f"block_idx={block_idx} 범위 초과 ({name} block 수: {n_blocks})") + return idx def _get_face_bbox(self, img: np.ndarray) -> List[float]: result = self.face_detector.detect_batch([img], [1.0]) @@ -113,5 +133,5 @@ def __repr__(self): return (f"{self.__class__.__name__}(" f"model={self.model_name}, dataset={self.dataset}, " f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " - f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, " + f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, block_idx={self.block_idx}," f"category={self.category},device={self.device})") \ No newline at end of file diff --git a/explainability/cam_explainer.py b/explainability/cam_explainer.py index 6254366..331ea74 100644 --- a/explainability/cam_explainer.py +++ b/explainability/cam_explainer.py @@ -58,7 +58,7 @@ def __repr__(self): return (f"{self.__class__.__name__}(" f"model={self.model_name}, dataset={self.dataset}, " f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " - f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, " + f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, block_idx={self.block_idx}," f"aug_smooth={self.aug_smooth}, eigen_smooth={self.eigen_smooth}, " f"colormap={self.colormap}, image_weight={self.image_weight}, " f"category={self.category},device={self.device})") From c1dd9412ce20f0b37c9b29a9a03f0d384fa05196 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 20:04:18 +0900 Subject: [PATCH 05/48] =?UTF-8?q?=F0=9F=92=9A=20Add=20PR=20CI=20workflow?= =?UTF-8?q?=20with=20ruff=20lint=20and=20pytest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/issue-auto-label.yml | 14 ------------ .github/workflows/issue-stale-clean.yml | 22 ------------------- .github/workflows/pr-ci.yml | 29 +++++++++++++++++++++++++ requirements.txt | 5 ++++- 4 files changed, 33 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/issue-auto-label.yml delete mode 100644 .github/workflows/issue-stale-clean.yml create mode 100644 .github/workflows/pr-ci.yml diff --git a/.github/workflows/issue-auto-label.yml b/.github/workflows/issue-auto-label.yml deleted file mode 100644 index ceec4a2..0000000 --- a/.github/workflows/issue-auto-label.yml +++ /dev/null @@ -1,14 +0,0 @@ -name: Auto Label Issues - -on: - issues: - types: [opened] - -jobs: - label: - runs-on: ubuntu-latest - steps: - - name: Label Bug - uses: actions-ecosystem/action-add-labels@v1 - with: - labels: bug diff --git a/.github/workflows/issue-stale-clean.yml b/.github/workflows/issue-stale-clean.yml deleted file mode 100644 index 7f90ca7..0000000 --- a/.github/workflows/issue-stale-clean.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Close stale issues - -on: - schedule: - - cron: "0 0 * * *" - -jobs: - stale: - runs-on: ubuntu-latest - steps: - - uses: actions/stale@v9 - with: - days-before-stale: 14 - days-before-close: 7 - stale-issue-label: "stale" - stale-issue-message: > - ⏳ 14일 동안 활동이 없어 자동으로 stale 처리되었습니다. - 추가 정보가 있다면 댓글로 다시 활성화해주세요! - - close-issue-message: > - 🔒 7일 동안 활동이 없어 Issue가 자동으로 닫혔습니다. - 필요하면 언제든 재오픈해주세요 🙂 diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml new file mode 100644 index 0000000..4bbaf9f --- /dev/null +++ b/.github/workflows/pr-ci.yml @@ -0,0 +1,29 @@ +name: PR CI + +on: + pull_request: + branches: [main, develop] + +jobs: + lint-and-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: "pip" + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Lint (ruff) + run: ruff check . --output-format=github + + - name: Format check (ruff) + run: ruff format --check . + + - name: Test (pytest) + run: pytest --tb=short -q \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ce368d3..72e2059 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,4 +40,7 @@ seaborn>=0.13.2 colorama>=0.4.6 # --- [Distributed Task Queue] --- -celery>=5.6.3 \ No newline at end of file +celery>=5.6.3 + +# --- [Linting & Formatting] --- +ruff>=0.9.0 \ No newline at end of file From c9f1041fc637f6d2423276be1c3b0e56058061dc Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 20:08:05 +0900 Subject: [PATCH 06/48] =?UTF-8?q?=F0=9F=92=9A=20Add=20PR=20auto-labeler=20?= =?UTF-8?q?with=20path-based=20label=20rules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/labeler.yml | 31 +++++++++++++++++++++++++++++++ .github/workflows/pr-label.yml | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 .github/labeler.yml create mode 100644 .github/workflows/pr-label.yml diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..8f69fee --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,31 @@ +model: + - changed-files: + - any-glob-to-any-file: ["src/model/**", "models/**", "*.pt", "*.pth"] + +data: + - changed-files: + - any-glob-to-any-file: ["data/**", "datasets/**", "src/data/**"] + +training: + - changed-files: + - any-glob-to-any-file: ["train*.py", "src/train/**", "configs/**"] + +api: + - changed-files: + - any-glob-to-any-file: ["api/**", "routers/**", "app.py"] + +test: + - changed-files: + - any-glob-to-any-file: ["tests/**", "test_*.py"] + +docs: + - changed-files: + - any-glob-to-any-file: ["docs/**", "*.md", "*.rst"] + +ci: + - changed-files: + - any-glob-to-any-file: [".github/**"] + +dependencies: + - changed-files: + - any-glob-to-any-file: ["requirements*.txt", "pyproject.toml", "setup.py"] \ No newline at end of file diff --git a/.github/workflows/pr-label.yml b/.github/workflows/pr-label.yml new file mode 100644 index 0000000..df3de40 --- /dev/null +++ b/.github/workflows/pr-label.yml @@ -0,0 +1,18 @@ +name: PR Auto Label + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + label: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/labeler@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + sync-labels: true # PR 파일 변경 시 라벨 재동기화 \ No newline at end of file From b84b267d078863269b70a4b77088513f56b9d722 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 20:26:44 +0900 Subject: [PATCH 07/48] =?UTF-8?q?=E2=9C=A8=20Add=20cam=5Fmult=5Fimage.py?= =?UTF-8?q?=20for=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/metrics/__init__.py | 0 explainability/metrics/cam_mult_image.py | 37 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 explainability/metrics/__init__.py create mode 100644 explainability/metrics/cam_mult_image.py diff --git a/explainability/metrics/__init__.py b/explainability/metrics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/explainability/metrics/cam_mult_image.py b/explainability/metrics/cam_mult_image.py new file mode 100644 index 0000000..c46751e --- /dev/null +++ b/explainability/metrics/cam_mult_image.py @@ -0,0 +1,37 @@ +import torch +import numpy as np +from typing import List, Callable +from explainability.metrics.perturbation_confidence import PerturbationConfidenceMetric + + +def multiply_tensor_with_cam(input_tensor: torch.Tensor, + cam: torch.Tensor): + """ Multiply an input tensor (after normalization) + with a pixel attribution map + """ + return input_tensor * cam + + +class CamMultImageConfidenceChange(PerturbationConfidenceMetric): + def __init__(self): + super(CamMultImageConfidenceChange, + self).__init__(multiply_tensor_with_cam) + + +class DropInConfidence(CamMultImageConfidenceChange): + def __init__(self): + super(DropInConfidence, self).__init__() + + def __call__(self, *args, **kwargs): + scores = super(DropInConfidence, self).__call__(*args, **kwargs) + scores = -scores + return np.maximum(scores, 0) + + +class IncreaseInConfidence(CamMultImageConfidenceChange): + def __init__(self): + super(IncreaseInConfidence, self).__init__() + + def __call__(self, *args, **kwargs): + scores = super(IncreaseInConfidence, self).__call__(*args, **kwargs) + return np.float32(scores > 0) \ No newline at end of file From cf97ec79ac1161ab28eee40d7887852237fdcea1 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 20:29:26 +0900 Subject: [PATCH 08/48] =?UTF-8?q?=E2=9C=A8=20Add=20perturbation=5Fconfiden?= =?UTF-8?q?ce.py=20for=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../metrics/perturbation_confidence.py | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 explainability/metrics/perturbation_confidence.py diff --git a/explainability/metrics/perturbation_confidence.py b/explainability/metrics/perturbation_confidence.py new file mode 100644 index 0000000..211ab8a --- /dev/null +++ b/explainability/metrics/perturbation_confidence.py @@ -0,0 +1,109 @@ +import torch +import numpy as np +from typing import List, Callable + +import numpy as np +import cv2 + + +class PerturbationConfidenceMetric: + def __init__(self, perturbation): + self.perturbation = perturbation + + def __call__(self, input_tensor: torch.Tensor, + cams: np.ndarray, + targets: List[Callable], + model: torch.nn.Module, + return_visualization=False, + return_diff=True): + + if return_diff: + with torch.no_grad(): + outputs = model(input_tensor) + scores = [target(output).cpu().numpy() + for target, output in zip(targets, outputs)] + scores = np.float32(scores) + + batch_size = input_tensor.size(0) + perturbated_tensors = [] + for i in range(batch_size): + cam = cams[i] + tensor = self.perturbation(input_tensor[i, ...].cpu(), + torch.from_numpy(cam)) + tensor = tensor.to(input_tensor.device) + perturbated_tensors.append(tensor.unsqueeze(0)) + perturbated_tensors = torch.cat(perturbated_tensors) + + with torch.no_grad(): + outputs_after_imputation = model(perturbated_tensors) + scores_after_imputation = [ + target(output).cpu().numpy() for target, output in zip( + targets, outputs_after_imputation)] + scores_after_imputation = np.float32(scores_after_imputation) + + if return_diff: + result = scores_after_imputation - scores + else: + result = scores_after_imputation + + if return_visualization: + return result, perturbated_tensors + else: + return result + + +class RemoveMostRelevantFirst: + def __init__(self, percentile, imputer): + self.percentile = percentile + self.imputer = imputer + + def __call__(self, input_tensor, mask): + imputer = self.imputer + if self.percentile != 'auto': + threshold = np.percentile(mask.cpu().numpy(), self.percentile) + binary_mask = np.float32(mask < threshold) + else: + _, binary_mask = cv2.threshold( + np.uint8(mask * 255), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + + binary_mask = torch.from_numpy(binary_mask) + binary_mask = binary_mask.to(mask.device) + return imputer(input_tensor, binary_mask) + + +class RemoveLeastRelevantFirst(RemoveMostRelevantFirst): + def __init__(self, percentile, imputer): + super(RemoveLeastRelevantFirst, self).__init__(percentile, imputer) + + def __call__(self, input_tensor, mask): + return super(RemoveLeastRelevantFirst, self).__call__( + input_tensor, 1 - mask) + + +class AveragerAcrossThresholds: + def __init__( + self, + imputer, + percentiles=[ + 10, + 20, + 30, + 40, + 50, + 60, + 70, + 80, + 90]): + self.imputer = imputer + self.percentiles = percentiles + + def __call__(self, + input_tensor: torch.Tensor, + cams: np.ndarray, + targets: List[Callable], + model: torch.nn.Module): + scores = [] + for percentile in self.percentiles: + imputer = self.imputer(percentile) + scores.append(imputer(input_tensor, cams, targets, model)) + return np.mean(np.float32(scores), axis=0) \ No newline at end of file From 92c30077936c7c2f60091574d95e065fa752dbeb Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 9 May 2026 20:31:34 +0900 Subject: [PATCH 09/48] =?UTF-8?q?=E2=9C=A8=20Add=20road.py=20for=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/metrics/road.py | 154 +++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 explainability/metrics/road.py diff --git a/explainability/metrics/road.py b/explainability/metrics/road.py new file mode 100644 index 0000000..905f84e --- /dev/null +++ b/explainability/metrics/road.py @@ -0,0 +1,154 @@ +import torch +import numpy as np +from scipy.sparse import lil_matrix, csc_matrix +from scipy.sparse.linalg import spsolve +from typing import List, Callable +from explainability.metrics.perturbation_confidence import ( + PerturbationConfidenceMetric, AveragerAcrossThresholds, + RemoveMostRelevantFirst, RemoveLeastRelevantFirst +) + +# The weights of the surrounding pixels +neighbors_weights = [((1, 1), 1 / 12), + ((0, 1), 1 / 6), + ((-1, 1), 1 / 12), + ((1, -1), 1 / 12), + ((0, -1), 1 / 6), + ((-1, -1), 1 / 12), + ((1, 0), 1 / 6), + ((-1, 0), 1 / 6)] + + +class NoisyLinearImputer: + def __init__(self, + noise: float = 0.01, + weighting: List[float] = neighbors_weights): + """ + Noisy linear imputation. + noise: magnitude of noise to add (absolute, set to 0 for no noise) + weighting: Weights of the neighboring pixels in the computation. + List of tuples of (offset, weight) + """ + self.noise = noise + self.weighting = neighbors_weights + + @staticmethod + def add_offset_to_indices(indices, offset, mask_shape): + """ Add the corresponding offset to the indices. + Return new indices plus a valid bit-vector. """ + cord1 = indices % mask_shape[1] + cord0 = indices // mask_shape[1] + cord0 += offset[0] + cord1 += offset[1] + valid = ((cord0 < 0) | (cord1 < 0) | + (cord0 >= mask_shape[0]) | + (cord1 >= mask_shape[1])) + return ~valid, indices + offset[0] * mask_shape[1] + offset[1] + + @staticmethod + def setup_sparse_system(mask, img, neighbors_weights): + """ Vectorized version to set up the equation system. + mask: (H, W)-tensor of missing pixels. + Image: (H, W, C)-tensor of all values. + Return (N,N)-System matrix, (N,C)-Right hand side for each of the C channels. + """ + maskflt = mask.flatten() + imgflat = img.reshape((img.shape[0], -1)) + # Indices that are imputed in the flattened mask: + indices = np.argwhere(maskflt == 0).flatten() + coords_to_vidx = np.zeros(len(maskflt), dtype=int) + coords_to_vidx[indices] = np.arange(len(indices)) + numEquations = len(indices) + # System matrix: + A = lil_matrix((numEquations, numEquations)) + b = np.zeros((numEquations, img.shape[0])) + # Sum of weights assigned: + sum_neighbors = np.ones(numEquations) + for n in neighbors_weights: + offset, weight = n[0], n[1] + # Take out outliers + valid, new_coords = NoisyLinearImputer.add_offset_to_indices( + indices, offset, mask.shape) + valid_coords = new_coords[valid] + valid_ids = np.argwhere(valid == 1).flatten() + # Add values to the right hand-side + has_values_coords = valid_coords[maskflt[valid_coords] > 0.5] + has_values_ids = valid_ids[maskflt[valid_coords] > 0.5] + b[has_values_ids, :] -= weight * imgflat[:, has_values_coords].T + # Add weights to the system (left hand side) +# Find coordinates in the system. + has_no_values = valid_coords[maskflt[valid_coords] < 0.5] + variable_ids = coords_to_vidx[has_no_values] + has_no_values_ids = valid_ids[maskflt[valid_coords] < 0.5] + A[has_no_values_ids, variable_ids] = weight + # Reduce weight for invalid + sum_neighbors[np.argwhere(valid == 0).flatten()] = \ + sum_neighbors[np.argwhere(valid == 0).flatten()] - weight + + A[np.arange(numEquations), np.arange(numEquations)] = -sum_neighbors + return A, b + + def __call__(self, img: torch.Tensor, mask: torch.Tensor): + """ Our linear inputation scheme. """ + """ + This is the function to do the linear infilling + img: original image (C,H,W)-tensor; + mask: mask; (H,W)-tensor + + """ + imgflt = img.reshape(img.shape[0], -1) + maskflt = mask.reshape(-1) + # Indices that need to be imputed. + indices_linear = np.argwhere(maskflt == 0).flatten() + # Set up sparse equation system, solve system. + A, b = NoisyLinearImputer.setup_sparse_system( + mask.numpy(), img.numpy(), neighbors_weights) + res = torch.tensor(spsolve(csc_matrix(A), b), dtype=torch.float) + + # Fill the values with the solution of the system. + img_infill = imgflt.clone() + img_infill[:, indices_linear] = res.t() + self.noise * \ + torch.randn_like(res.t()) + + return img_infill.reshape_as(img) + + +class ROADMostRelevantFirst(PerturbationConfidenceMetric): + def __init__(self, percentile=80): + super(ROADMostRelevantFirst, self).__init__( + RemoveMostRelevantFirst(percentile, NoisyLinearImputer())) + + +class ROADLeastRelevantFirst(PerturbationConfidenceMetric): + def __init__(self, percentile=20): + super(ROADLeastRelevantFirst, self).__init__( + RemoveLeastRelevantFirst(percentile, NoisyLinearImputer())) + + +class ROADMostRelevantFirstAverage(AveragerAcrossThresholds): + def __init__(self, percentiles=[10, 20, 30, 40, 50, 60, 70, 80, 90]): + super(ROADMostRelevantFirstAverage, self).__init__( + ROADMostRelevantFirst, percentiles) + + +class ROADLeastRelevantFirstAverage(AveragerAcrossThresholds): + def __init__(self, percentiles=[10, 20, 30, 40, 50, 60, 70, 80, 90]): + super(ROADLeastRelevantFirstAverage, self).__init__( + ROADLeastRelevantFirst, percentiles) + + +class ROADCombined: + def __init__(self, percentiles=[10, 20, 30, 40, 50, 60, 70, 80, 90]): + self.percentiles = percentiles + self.morf_averager = ROADMostRelevantFirstAverage(percentiles) + self.lerf_averager = ROADLeastRelevantFirstAverage(percentiles) + + def __call__(self, + input_tensor: torch.Tensor, + cams: np.ndarray, + targets: List[Callable], + model: torch.nn.Module): + + scores_lerf = self.lerf_averager(input_tensor, cams, targets, model) + scores_morf = self.morf_averager(input_tensor, cams, targets, model) + return (scores_lerf - scores_morf) / 2 \ No newline at end of file From f060223d0e33f9d95302c03d3381721950fcb3e9 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 09:34:58 +0900 Subject: [PATCH 10/48] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20SegEigenCAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/eigenvalue_explainer.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/explainability/eigenvalue_explainer.py b/explainability/eigenvalue_explainer.py index 5eb95c7..79f0927 100644 --- a/explainability/eigenvalue_explainer.py +++ b/explainability/eigenvalue_explainer.py @@ -1,4 +1,4 @@ -from pytorch_grad_cam import EigenCAM, EigenGradCAM, KPCA_CAM, SegEigenCAM +from pytorch_grad_cam import EigenCAM, EigenGradCAM, KPCA_CAM from explainability.cam_explainer import CAMExplainer class EigenCAMExplainer(CAMExplainer): @@ -33,15 +33,3 @@ def _build_cam(self): def __repr__(self): return super().__repr__().rstrip(")") + f", kernel={self.kernel}, gamma={self.gamma})" - -class SegEigenCAMExplainer(CAMExplainer): - """ - Like EigenCAM but with gradient weighting (absolute gradients ⊙ activations ) - before SVD and sign correction to fix SVD sign ambiguity - designed for semantic segmentation - """ - def _build_cam(self): - return SegEigenCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn) - - - From 36b504964b1498b1aee4d7d50dc2f089ec971800 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 10:07:54 +0900 Subject: [PATCH 11/48] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20AblationCAM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/perturbation_explainer.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/explainability/perturbation_explainer.py b/explainability/perturbation_explainer.py index 28730ed..1753954 100644 --- a/explainability/perturbation_explainer.py +++ b/explainability/perturbation_explainer.py @@ -1,24 +1,5 @@ -from pytorch_grad_cam import AblationCAM, ScoreCAM, FEM +from pytorch_grad_cam import ScoreCAM, FEM from explainability.cam_explainer import CAMExplainer - - -class AblationCAMExplainer(CAMExplainer): - """ - Zero out activations and measure how the output drops - (this repository includes a fast batched implementation) - """ - def __init__(self, batch_size: int = 32, ratio_channels_to_ablate: float = 1.0, **kwargs): - self.batch_size = batch_size - self.ratio_channels_to_ablate = ratio_channels_to_ablate - super().__init__(**kwargs) - - def _build_cam(self): - return AblationCAM(model=self.model, target_layers=self.target_layers, reshape_transform=self.reshape_fn, - batch_size=self.batch_size, ratio_channels_to_ablate=self.ratio_channels_to_ablate) - def __repr__(self): - return super().__repr__().rstrip(")") + f", ratio={self.ratio_channels_to_ablate})" - - class ScoreCAMExplainer(CAMExplainer): """ Perbutate the image by the scaled activation From d24335674b700854cfbe850c87b77578615f7162 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 11:47:37 +0900 Subject: [PATCH 12/48] =?UTF-8?q?=E2=9C=A8=20Add=20remove=5Fpadding=5Fand?= =?UTF-8?q?=5Fresize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/utils/image.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/explainability/utils/image.py b/explainability/utils/image.py index 83b90fa..92a5ba4 100644 --- a/explainability/utils/image.py +++ b/explainability/utils/image.py @@ -44,4 +44,18 @@ def deprocess_image(tensor): img = (img * std) + mean img = np.clip(img, 0, 1) # range(0~1) - return np.float32(img) \ No newline at end of file + return np.float32(img) + +def remove_padding_and_resize(cam, orig_shape, target_shape): + orig_h, orig_w = orig_shape[:2] # (H,W,3) + target_h, target_w = target_shape[2:] # (1,3,H,W) + + scale = max(target_h, target_w) / max(orig_h, orig_w) + new_h, new_w = int(orig_h * scale), int(orig_w * scale) + + pad_h = (target_h - new_h) // 2 + pad_w = (target_w - new_w) // 2 + + # Remove Padding + unpadded_cam = cam[pad_h:pad_h+new_h, pad_w:pad_w+new_w] + return cv2.resize(unpadded_cam, (orig_w, orig_h)) \ No newline at end of file From 294c7153f2e87d5a08a7c2b2aaafb7e07e297ec1 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 11:53:36 +0900 Subject: [PATCH 13/48] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20display?= =?UTF-8?q?=5Fheatmap=20with=20new=20arguements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_explainer.py | 40 +++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/explainability/cam_explainer.py b/explainability/cam_explainer.py index 331ea74..837a7db 100644 --- a/explainability/cam_explainer.py +++ b/explainability/cam_explainer.py @@ -3,27 +3,32 @@ import numpy as np from abc import ABC, abstractmethod from explainability.utils.model_targets import BinaryClassifierOutputTarget -from explainability.utils.image import show_cam_on_image, deprocess_image +from explainability.utils.image import show_cam_on_image, deprocess_image, remove_padding_and_resize from .base_explainer import BaseExplainer class CAMExplainer(BaseExplainer, ABC): def __init__(self, aug_smooth: bool = False, eigen_smooth: bool = False, - colormap: int = cv2.COLORMAP_JET, - image_weight: float = 0.5, **kwargs): super().__init__(**kwargs) self.aug_smooth = aug_smooth # aug_smooth has the effect of beter centering the CAM around the objects self.eigen_smooth = eigen_smooth # # eigen_smooth has the effect of removing a lot of noise - self.colormap = colormap - self.image_weight = image_weight self.cam = self._build_cam() @abstractmethod def _build_cam(self): ... - def display_heatmap(self, img_path): + def _build_grayscale_cam(self, img_path: str): + """ + Generate a grayscale CAM and its associated image tensors. + + Returns: + tuple: (grayscale_cam, tensor, face) + - grayscale_cam (np.ndarray): CAM result in (H, W), range [0, 1]. + - tensor (torch.Tensor): Preprocessed input in (1, 3, H, W). + - face (np.ndarray): Original image in (H, W, 3), uint8. + """ face, tensor = self._get_transform(img_path) tensor = tensor.to(self.device) # (1,3,H,W) targets = [BinaryClassifierOutputTarget(self.category)] @@ -35,25 +40,22 @@ def display_heatmap(self, img_path): eigen_smooth = self.eigen_smooth ) # (1,H,W) - return grayscale_cam[0], tensor + return grayscale_cam[0], tensor, face + + def display_heatmap(self, img_path: str, colormap: int = cv2.COLORMAP_JET, image_weight: float = 0.5,): + grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) - def display_cam_on_image(self, img_path): - grayscale_cam, tensor = self.display_heatmap(img_path) + cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) heatmap = show_cam_on_image( - deprocess_image(tensor), # (H,W,3), range(0~1), np.ndarray - grayscale_cam, # (H,W), range(0~1), np.ndarray - use_rgb=True, - colormap = self.colormap, - image_weight=self.image_weight) + np.float32(face) / 255.0, # (H,W,3), range(0~1), np.ndarray + cam_recovered, # (H,W), range(0~1), np.ndarray + use_rgb =True, + colormap = colormap, + image_weight= image_weight) return heatmap # (H,W,3), range(0~255), np.uint8 - def display_inverse_cam(self, img_path): - grayscale_cam, _ = self.display_heatmap(img_path) - - return 1 - grayscale_cam # (H,W), range(0~1), np.float32 - def __repr__(self): return (f"{self.__class__.__name__}(" f"model={self.model_name}, dataset={self.dataset}, " From fc436b8cb5a2a346510aa9c14558ef13b5d4a283 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 12:43:17 +0900 Subject: [PATCH 14/48] =?UTF-8?q?=E2=9C=A8=20Add=20display=5Fcontour=5Fon?= =?UTF-8?q?=5Fimage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_explainer.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/explainability/cam_explainer.py b/explainability/cam_explainer.py index 837a7db..4b597ed 100644 --- a/explainability/cam_explainer.py +++ b/explainability/cam_explainer.py @@ -42,9 +42,10 @@ def _build_grayscale_cam(self, img_path: str): return grayscale_cam[0], tensor, face - def display_heatmap(self, img_path: str, colormap: int = cv2.COLORMAP_JET, image_weight: float = 0.5,): + def display_heatmap_on_image(self, img_path: str, + colormap: int = cv2.COLORMAP_JET, + image_weight: float = 0.5,): grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) - cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) heatmap = show_cam_on_image( @@ -56,6 +57,20 @@ def display_heatmap(self, img_path: str, colormap: int = cv2.COLORMAP_JET, image return heatmap # (H,W,3), range(0~255), np.uint8 + def display_contour_on_image(self, img_path: str, + threshold: float = 0.5, + thickness: int = 2, + ): + grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) + cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) + + mask = (cam_recovered > threshold).astype(np.uint8) * 255 + contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + annotated_img = cv2.drawContours(face.copy(), contours, -1, (255, 0, 0), thickness) + + return annotated_img + + def __repr__(self): return (f"{self.__class__.__name__}(" f"model={self.model_name}, dataset={self.dataset}, " From 3a6b9bbef0bdbe6af67d0be3736a6ad08651deba Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 15:00:14 +0900 Subject: [PATCH 15/48] =?UTF-8?q?=E2=9C=A8=20Add=20display=5Fbbox=5Fon=5Fi?= =?UTF-8?q?mage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_explainer.py | 147 ++++++++++++++++++++++---------- 1 file changed, 103 insertions(+), 44 deletions(-) diff --git a/explainability/cam_explainer.py b/explainability/cam_explainer.py index 4b597ed..bec4ffd 100644 --- a/explainability/cam_explainer.py +++ b/explainability/cam_explainer.py @@ -5,14 +5,41 @@ from explainability.utils.model_targets import BinaryClassifierOutputTarget from explainability.utils.image import show_cam_on_image, deprocess_image, remove_padding_and_resize from .base_explainer import BaseExplainer + + +def _draw_label(img: np.ndarray, text: str, x: int, y: int, prob_ratio: float = 1.0): + """Confidence label with a clean pill-style background. + + prob_ratio: 0~1, controls bg color (green→red) + """ + font = cv2.FONT_HERSHEY_SIMPLEX + font_scale = 0.52 + thickness = 1 + pad = 4 + + (tw, th), baseline = cv2.getTextSize(text, font, font_scale, thickness) + + rx1, ry1 = x - pad, y - th - pad + rx2, ry2 = x + tw + pad, y + baseline + pad + + r = int(min(255, 2 * prob_ratio * 255)) + g = int(min(255, 2 * (1 - prob_ratio) * 255)) + bg_color = (r, g, 30) # RGB: green → red + + overlay = img.copy() + cv2.rectangle(overlay, (rx1, ry1), (rx2, ry2), bg_color, -1) + cv2.addWeighted(overlay, 0.75, img, 0.25, 0, img) + cv2.putText(img, text, (x, y), font, font_scale, (255, 255, 255), thickness, cv2.LINE_AA) + + class CAMExplainer(BaseExplainer, ABC): def __init__(self, aug_smooth: bool = False, eigen_smooth: bool = False, **kwargs): super().__init__(**kwargs) - self.aug_smooth = aug_smooth # aug_smooth has the effect of beter centering the CAM around the objects - self.eigen_smooth = eigen_smooth # # eigen_smooth has the effect of removing a lot of noise + self.aug_smooth = aug_smooth + self.eigen_smooth = eigen_smooth self.cam = self._build_cam() @abstractmethod @@ -20,63 +47,95 @@ def _build_cam(self): ... def _build_grayscale_cam(self, img_path: str): - """ - Generate a grayscale CAM and its associated image tensors. - - Returns: - tuple: (grayscale_cam, tensor, face) - - grayscale_cam (np.ndarray): CAM result in (H, W), range [0, 1]. - - tensor (torch.Tensor): Preprocessed input in (1, 3, H, W). - - face (np.ndarray): Original image in (H, W, 3), uint8. - """ face, tensor = self._get_transform(img_path) - tensor = tensor.to(self.device) # (1,3,H,W) + tensor = tensor.to(self.device) targets = [BinaryClassifierOutputTarget(self.category)] grayscale_cam = self.cam( - input_tensor = tensor, # (1,3,H,W) - targets = targets, - aug_smooth = self.aug_smooth, - eigen_smooth = self.eigen_smooth - ) # (1,H,W) - - return grayscale_cam[0], tensor, face - - def display_heatmap_on_image(self, img_path: str, + input_tensor=tensor, + targets=targets, + aug_smooth=self.aug_smooth, + eigen_smooth=self.eigen_smooth, + ) + return grayscale_cam[0], tensor, face + + def _prepare_cam(self, img_path: str): + """Build grayscale CAM and recover it to original face resolution.""" + grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) + cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) + return cam_recovered, face + + def _get_binary_mask(self, cam: np.ndarray, threshold: float | str): + if threshold != 'auto': + t = np.percentile(cam, threshold * 100) + binary_mask = (cam > t).astype(np.uint8) * 255 + else: + _, binary_mask = cv2.threshold( + np.uint8(cam * 255), 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU + ) + return binary_mask + + def display_heatmap_on_image(self, + img_path: str, colormap: int = cv2.COLORMAP_JET, - image_weight: float = 0.5,): - grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) - cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) + image_weight: float = 0.5): + cam_recovered, face = self._prepare_cam(img_path) heatmap = show_cam_on_image( - np.float32(face) / 255.0, # (H,W,3), range(0~1), np.ndarray - cam_recovered, # (H,W), range(0~1), np.ndarray - use_rgb =True, - colormap = colormap, - image_weight= image_weight) - - return heatmap # (H,W,3), range(0~255), np.uint8 + np.float32(face) / 255.0, + cam_recovered, + use_rgb=True, + colormap=colormap, + image_weight=image_weight, + ) + return heatmap # (H,W,3), range(0~255), np.uint8 - def display_contour_on_image(self, img_path: str, - threshold: float = 0.5, - thickness: int = 2, - ): - grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) - cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) + def display_contour_on_image(self, + img_path: str, + threshold: float | str = 'auto', + thickness: int = 2): + cam_recovered, face = self._prepare_cam(img_path) - mask = (cam_recovered > threshold).astype(np.uint8) * 255 - contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + binary_mask = self._get_binary_mask(cam_recovered, threshold) + + contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) annotated_img = cv2.drawContours(face.copy(), contours, -1, (255, 0, 0), thickness) - return annotated_img - + def display_bbox_on_image(self, + img_path: str, + threshold: float | str = 'auto', + thickness: int = 2): + cam_recovered, face = self._prepare_cam(img_path) + + binary_mask = self._get_binary_mask(cam_recovered, threshold) + + contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + annotated_img = face.copy() + for c in contours: + x, y, w, h = cv2.boundingRect(c) + roi_prob = float(np.max(cam_recovered[y:y + h, x:x + w])) + + r = int(min(255, 2 * roi_prob * 255)) + g = int(min(255, 2 * (1 - roi_prob) * 255)) + box_color = (r, g, 30) # RGB + + cv2.rectangle(annotated_img, (x, y), (x + w, y + h), box_color, thickness) + _draw_label( + annotated_img, + text=f"{roi_prob * 100:.1f}%", + x=x, + y=max(y - 6, 14), + prob_ratio=roi_prob, + ) + + return annotated_img + def __repr__(self): return (f"{self.__class__.__name__}(" f"model={self.model_name}, dataset={self.dataset}, " f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, block_idx={self.block_idx}," f"aug_smooth={self.aug_smooth}, eigen_smooth={self.eigen_smooth}, " - f"colormap={self.colormap}, image_weight={self.image_weight}, " - f"category={self.category},device={self.device})") - \ No newline at end of file + f"category={self.category},device={self.device})") \ No newline at end of file From 62561ddde6d60f4cdf3e63298c3d256edde86d3e Mon Sep 17 00:00:00 2001 From: seoyunje Date: Mon, 11 May 2026 19:11:13 +0900 Subject: [PATCH 16/48] =?UTF-8?q?=E2=9C=A8=20Add=20BinaryClassifierOutputS?= =?UTF-8?q?igmoidTarget?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/utils/model_targets.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/explainability/utils/model_targets.py b/explainability/utils/model_targets.py index 62ba03a..5ec0870 100644 --- a/explainability/utils/model_targets.py +++ b/explainability/utils/model_targets.py @@ -1,3 +1,5 @@ +import torch + class BinaryClassifierOutputTarget: def __init__(self, category): self.category = category @@ -8,4 +10,15 @@ def __call__(self, model_output): else: sign = -1 return model_output * sign + +class BinaryClassifierOutputSigmoidTarget: + def __init__(self, category): + self.category = category + + def __call__(self, model_output): + if self.category == 1: + sign = 1 + else: + sign = -1 + return torch.sigmoid(model_output) * sign \ No newline at end of file From 01cc4fe1294d8d67c6865396c788fa9f190bb5a7 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Tue, 12 May 2026 19:13:01 +0900 Subject: [PATCH 17/48] =?UTF-8?q?=E2=9C=A8=20Create=20CAMEvaluator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_evaluator.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 explainability/cam_evaluator.py diff --git a/explainability/cam_evaluator.py b/explainability/cam_evaluator.py new file mode 100644 index 0000000..d7f4017 --- /dev/null +++ b/explainability/cam_evaluator.py @@ -0,0 +1,42 @@ +import numpy as np +from typing import List, Dict, Literal +from explainability.utils.model_targets import BinaryClassifierOutputSigmoidTarget +from explainability.cam_explainer import CAMExplainer +from explainability.metrics.road import ( + ROADLeastRelevantFirstAverage, ROADMostRelevantFirstAverage, + ROADCombined +) + +METRIC_REGISTRY = { + "MORF": ROADMostRelevantFirstAverage, + "LERF": ROADLeastRelevantFirstAverage, + "Combined": ROADCombined, +} + +class CAMEvaluator: + def __init__(self, + cam_explainer: CAMExplainer, + cam_metric: Literal["Combined", "MORF", "LERF"] = "Combined", # Combined, MORF, LERF + percentiles: List[int] = [10,20,30,40,50,60,70,80,90], + ): + self.cam_explainer = cam_explainer + self.cam_metric_name = cam_metric + self.percentiles = percentiles + self.cam_metric = METRIC_REGISTRY[cam_metric](percentiles) + + def evaluate(self, img_path: str) -> float: + grayscale_cam, tensor, _ = self.cam_explainer._build_grayscale_cam(img_path) + targets = [BinaryClassifierOutputSigmoidTarget(self.cam_explainer.category)] + + return self.cam_metric( + input_tensor = tensor, # (1,C,H,W) + cams = grayscale_cam[np.newaxis, ...], #(1,H,W) + targets = targets, + model = self.cam_explainer.model, + )[0] + + def __repr__(self): + return ( + f"CAMEvaluator(explainer={self.cam_explainer.__class__.__name__}, " + f"metric={self.cam_metric_name}, percentiles={self.percentiles})" + ) \ No newline at end of file From d67a42638a97bbc3a447438f77b160586ef92d4e Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 13 May 2026 09:32:29 +0900 Subject: [PATCH 18/48] =?UTF-8?q?=F0=9F=8E=A8=20Add=20Reproductability=20f?= =?UTF-8?q?or=20same=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_evaluator.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/explainability/cam_evaluator.py b/explainability/cam_evaluator.py index d7f4017..a4aa46a 100644 --- a/explainability/cam_evaluator.py +++ b/explainability/cam_evaluator.py @@ -1,4 +1,5 @@ import numpy as np +import torch from typing import List, Dict, Literal from explainability.utils.model_targets import BinaryClassifierOutputSigmoidTarget from explainability.cam_explainer import CAMExplainer @@ -18,13 +19,18 @@ def __init__(self, cam_explainer: CAMExplainer, cam_metric: Literal["Combined", "MORF", "LERF"] = "Combined", # Combined, MORF, LERF percentiles: List[int] = [10,20,30,40,50,60,70,80,90], + seed: int = 2026 ): + self.seed = seed self.cam_explainer = cam_explainer self.cam_metric_name = cam_metric self.percentiles = percentiles self.cam_metric = METRIC_REGISTRY[cam_metric](percentiles) def evaluate(self, img_path: str) -> float: + torch.manual_seed(self.seed) + np.random.seed(self.seed) + grayscale_cam, tensor, _ = self.cam_explainer._build_grayscale_cam(img_path) targets = [BinaryClassifierOutputSigmoidTarget(self.cam_explainer.category)] From 9d0ce5b91ef7463defc45820d1d55e86bf06ac65 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 13 May 2026 09:47:01 +0900 Subject: [PATCH 19/48] =?UTF-8?q?=F0=9F=9A=9A=20Move=20base,cam,gradient,e?= =?UTF-8?q?igenvalue,perturbation,misc=20explainer.py=20to=20explainer=20f?= =?UTF-8?q?older?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/__init__.py | 13 +++++++++++++ explainability/cam_evaluator.py | 2 +- explainability/{ => explainer}/base_explainer.py | 0 explainability/{ => explainer}/cam_explainer.py | 2 +- .../{ => explainer}/eigenvalue_explainer.py | 2 +- .../{ => explainer}/gradient_explainer.py | 2 +- explainability/{ => explainer}/misc_explainer.py | 2 +- .../{ => explainer}/perturbation_explainer.py | 2 +- 8 files changed, 19 insertions(+), 6 deletions(-) rename explainability/{ => explainer}/base_explainer.py (100%) rename explainability/{ => explainer}/cam_explainer.py (98%) rename explainability/{ => explainer}/eigenvalue_explainer.py (95%) rename explainability/{ => explainer}/gradient_explainer.py (96%) rename explainability/{ => explainer}/misc_explainer.py (84%) rename explainability/{ => explainer}/perturbation_explainer.py (92%) diff --git a/explainability/__init__.py b/explainability/__init__.py index e69de29..b547123 100644 --- a/explainability/__init__.py +++ b/explainability/__init__.py @@ -0,0 +1,13 @@ +from explainability.explainer.base_explainer import BaseExplainer +from explainability.explainer.cam_explainer import CAMExplainer +from explainability.explainer.gradient_explainer import ( + GradCAMExplainer, GradCAMPlusPlusExplainer, GradCAMElementWiseExplainer, + XGradCAMExplainer, HiResCAMExplainer +) +from explainability.explainer.eigenvalue_explainer import ( + EigenCAMExplainer, EigenGradCAMExplainer, KPCACAMExplainer +) +from explainability.explainer.perturbation_explainer import ( + ScoreCAMExplainer, FEMExplainer +) +from explainability.explainer.misc_explainer import LayerCAMExplainer \ No newline at end of file diff --git a/explainability/cam_evaluator.py b/explainability/cam_evaluator.py index a4aa46a..c325811 100644 --- a/explainability/cam_evaluator.py +++ b/explainability/cam_evaluator.py @@ -2,7 +2,7 @@ import torch from typing import List, Dict, Literal from explainability.utils.model_targets import BinaryClassifierOutputSigmoidTarget -from explainability.cam_explainer import CAMExplainer +from explainability.explainer.cam_explainer import CAMExplainer from explainability.metrics.road import ( ROADLeastRelevantFirstAverage, ROADMostRelevantFirstAverage, ROADCombined diff --git a/explainability/base_explainer.py b/explainability/explainer/base_explainer.py similarity index 100% rename from explainability/base_explainer.py rename to explainability/explainer/base_explainer.py diff --git a/explainability/cam_explainer.py b/explainability/explainer/cam_explainer.py similarity index 98% rename from explainability/cam_explainer.py rename to explainability/explainer/cam_explainer.py index bec4ffd..5764ef3 100644 --- a/explainability/cam_explainer.py +++ b/explainability/explainer/cam_explainer.py @@ -4,7 +4,7 @@ from abc import ABC, abstractmethod from explainability.utils.model_targets import BinaryClassifierOutputTarget from explainability.utils.image import show_cam_on_image, deprocess_image, remove_padding_and_resize -from .base_explainer import BaseExplainer +from explainability.explainer.base_explainer import BaseExplainer def _draw_label(img: np.ndarray, text: str, x: int, y: int, prob_ratio: float = 1.0): diff --git a/explainability/eigenvalue_explainer.py b/explainability/explainer/eigenvalue_explainer.py similarity index 95% rename from explainability/eigenvalue_explainer.py rename to explainability/explainer/eigenvalue_explainer.py index 79f0927..9aa2633 100644 --- a/explainability/eigenvalue_explainer.py +++ b/explainability/explainer/eigenvalue_explainer.py @@ -1,5 +1,5 @@ from pytorch_grad_cam import EigenCAM, EigenGradCAM, KPCA_CAM -from explainability.cam_explainer import CAMExplainer +from explainability.explainer.cam_explainer import CAMExplainer class EigenCAMExplainer(CAMExplainer): """ diff --git a/explainability/gradient_explainer.py b/explainability/explainer/gradient_explainer.py similarity index 96% rename from explainability/gradient_explainer.py rename to explainability/explainer/gradient_explainer.py index 0282fd6..3767fc7 100644 --- a/explainability/gradient_explainer.py +++ b/explainability/explainer/gradient_explainer.py @@ -2,7 +2,7 @@ GradCAM, GradCAMPlusPlus, GradCAMElementWise, XGradCAM, HiResCAM ) -from explainability.cam_explainer import CAMExplainer +from explainability.explainer.cam_explainer import CAMExplainer class GradCAMExplainer(CAMExplainer): diff --git a/explainability/misc_explainer.py b/explainability/explainer/misc_explainer.py similarity index 84% rename from explainability/misc_explainer.py rename to explainability/explainer/misc_explainer.py index db6afa0..85cb38f 100644 --- a/explainability/misc_explainer.py +++ b/explainability/explainer/misc_explainer.py @@ -1,5 +1,5 @@ from pytorch_grad_cam import LayerCAM, RandomCAM -from explainability.cam_explainer import CAMExplainer +from explainability.explainer.cam_explainer import CAMExplainer class LayerCAMExplainer(CAMExplainer): diff --git a/explainability/perturbation_explainer.py b/explainability/explainer/perturbation_explainer.py similarity index 92% rename from explainability/perturbation_explainer.py rename to explainability/explainer/perturbation_explainer.py index 1753954..cb460aa 100644 --- a/explainability/perturbation_explainer.py +++ b/explainability/explainer/perturbation_explainer.py @@ -1,5 +1,5 @@ from pytorch_grad_cam import ScoreCAM, FEM -from explainability.cam_explainer import CAMExplainer +from explainability.explainer.cam_explainer import CAMExplainer class ScoreCAMExplainer(CAMExplainer): """ Perbutate the image by the scaled activation From 8c43e5e9f153978c48ff20fffd74f312d1a4c0f8 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 13 May 2026 15:34:47 +0900 Subject: [PATCH 20/48] =?UTF-8?q?=E2=9C=A8=20Create=20cam=20router=20for?= =?UTF-8?q?=20DeepFake=20XAI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/main.py | 51 ++++++++++++++++++++++++++------------------- App/routes/cam.py | 3 +++ App/utils/common.py | 12 +++++++++-- 3 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 App/routes/cam.py diff --git a/App/main.py b/App/main.py index 530812c..95c3c97 100644 --- a/App/main.py +++ b/App/main.py @@ -1,44 +1,50 @@ import os import sys -from pathlib import Path import warnings import logging +from pathlib import Path from dotenv import load_dotenv # ------ 상위 폴더 경로 설정 -------- +REQUIRED_PACKAGES = ["deepguard", "inference", "explainability", "preprocess"] + current_file = Path(__file__).resolve() -project_root = current_file.parent.parent # App의 상위 폴더 +project_root = current_file.parent.parent -if str(project_root) not in sys.path: - sys.path.insert(0, str(project_root)) +for pkg in REQUIRED_PACKAGES: + pkg_path = str(project_root / pkg) + if pkg_path not in sys.path: + sys.path.insert(0, pkg_path) +# ------ .env 파일 가져오기 ------ load_dotenv() # -------- Huggingface_Hub 인증 ---------- if os.getenv("HF_TOKEN"): os.environ["HF_TOKEN"] = os.getenv("HF_TOKEN") +else: + print("[Warning] HF_TOKEN is not set") from fastapi import FastAPI from fastapi.staticfiles import StaticFiles from fastapi.exceptions import RequestValidationError from starlette.exceptions import HTTPException as StarletteHTTPException from fastapi.middleware.cors import CORSMiddleware -from starlette.middleware.sessions import SessionMiddleware -from routes import auth, home, inference, image, video -from utils.common import lifespan -from utils import exc_handler, middleware +# from starlette.middleware.sessions import SessionMiddleware +from routes import auth, home, inference, image, video, cam +from utils import exc_handler, middleware, common -# 가상 인스턴스 생성 -app = FastAPI(lifespan=lifespan) +# 가상 FastAPI 인스턴스 생성 +app = FastAPI(lifespan=common.lifespan) -# StaticFile 등록하기 +# StaticFile 등록 (이미지, 비디오 파일) app.mount("/static", StaticFiles(directory="static"), name="static") -# Cross Origin Resource Sharing +# CORS Setup for cross-origin requests +# FrontEnd: React, BackEnd: FastAPI origins_str = os.getenv("CORS_ALLOWED_ORIGINS") allowed_origins = [origin.strip() for origin in origins_str.split(",")] - app.add_middleware(CORSMiddleware, allow_origins=allowed_origins, allow_methods=["*"], @@ -52,16 +58,17 @@ # app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, max_age=3600) # 세션 미들웨어 등록 - Redis 이용 -app.add_middleware(middleware.RedisSessionMiddleware, max_age=7200) +app.add_middleware(middleware.RedisSessionMiddleware, max_age=3600) -# 라우터 등록 -app.include_router(auth.router) -app.include_router(home.router) -app.include_router(inference.router) -app.include_router(image.router) -app.include_router(video.router) +# 라우터 등록 (View: Router, Controller: Service) +app.include_router(auth.router) # 로그인, 로그아웃, 회원가입 +app.include_router(home.router) # 세션 유저 정보 가져오기 +app.include_router(inference.router) # 이미지, 비디오 비동기 추론 접수, 추론 값 가져오기 +app.include_router(image.router) # 이미지 삭제 및 가져오기 +app.include_router(video.router) # 비디오 삭제 및 가져오기 +app.include_router(cam.router) # 딥페이크 이미지, 비디오 위조 흔적 표시 -# Custom HTTPException Handler +# 커스텀 예외 처리: HTTPException app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) -# Custom RequestValidationError Handler +# 커스텀 예외 처리: RequestValidationError app.add_exception_handler(RequestValidationError, exc_handler.validation_exception_handler) \ No newline at end of file diff --git a/App/routes/cam.py b/App/routes/cam.py new file mode 100644 index 0000000..eb0a78c --- /dev/null +++ b/App/routes/cam.py @@ -0,0 +1,3 @@ +from fastapi import APIRouter + +router = APIRouter(prefix="/cam", tags=["cam"]) \ No newline at end of file diff --git a/App/utils/common.py b/App/utils/common.py index c49d71c..a72d30c 100644 --- a/App/utils/common.py +++ b/App/utils/common.py @@ -2,12 +2,20 @@ from db.database import engine from contextlib import asynccontextmanager +# Uvicorn이 앱 시작/종료 시 asynccontextmanager를 호출함 @asynccontextmanager async def lifespan(app: FastAPI): - # FastAPI 인스턴스 기동시 필요한 작업 수행 + + # ── Startup ────────────────────────────────────────── + # Event Loop 위에서 실행, DB 연결 풀 초기화 print("Starting up...") + + # yield 이후 app이 실제 요청을 처리하기 시작함 yield - # FastAPI 인스턴스 종료시 필요한 작업 수행 + # ── Shutdown ───────────────────────────────────────── + # 새 요청은 차단되고, 진행 중인 요청 완료 후 여기로 진입 print("Shutting down...") + + # SQLAlchemy 비동기 엔진의 커넥션 풀 전체 반환 및 종료 await engine.dispose() \ No newline at end of file From ef7fb4c714ef3e91118892825bbd616cebf45ae4 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 13 May 2026 18:36:08 +0900 Subject: [PATCH 21/48] =?UTF-8?q?=F0=9F=94=A5=20Remove=20legacy=20file(set?= =?UTF-8?q?up.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- setup.py | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 setup.py diff --git a/setup.py b/setup.py deleted file mode 100644 index 16d3c3c..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -import setuptools - -with open("README.md", mode='r', encoding='utf-8') as fh: - long_description = fh.read() - -setuptools.setup( - name = 'deepguard', - version = '0.2.1', - author = 'seoyunje', - author_email = "seoyunje2001@gmail.com", - description = "Multi-Scale Efficient Vision Transformer for Robust Deepfake Detection", - long_description = long_description, - long_description_content_type ='text/markdown', - url = 'https://github.com/HanMoonSub/DeepGuard', - project_urls = { - "Bug Tracker": "https://github.com/HanMoonSub/DeepGuard/issues", - "Source Code": "https://github.com/HanMoonSub/DeepGuard", - }, - classifiers = [ - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Topic :: Scientific/Engineering :: Artificial Intelligence', - ], - packages = setuptools.find_packages(exclude=['Attention', 'preprocess','inference']), - python_requires = '>=3.10', - install_requires = [] -) \ No newline at end of file From 91d4981e580f59a78a4b7907c2a71628bc2842d6 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 13 May 2026 18:47:49 +0900 Subject: [PATCH 22/48] =?UTF-8?q?=E2=9C=A8=20Create=20pyproject.toml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + App/celery_app.py | 10 +------ App/main.py | 15 ----------- App/services/inference_svc.py | 2 +- App/services/video_svc.py | 2 +- pyproject.toml | 51 +++++++++++++++++++++++++++++++++++ 6 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore index da944b5..d0e86dc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +*.egg-info # [FastAPI / Python - App 폴더 관련] **/__pycache__/ diff --git a/App/celery_app.py b/App/celery_app.py index 027ba15..df5fd33 100644 --- a/App/celery_app.py +++ b/App/celery_app.py @@ -1,18 +1,10 @@ import os -import sys -from pathlib import Path from celery import Celery from dotenv import load_dotenv -current_file = Path(__file__).resolve() -project_root = current_file.parent.parent - -if str(project_root) not in sys.path: - sys.path.insert(0, str(project_root)) - +# ------ .env 파일 가져오기 ------ load_dotenv() -# 기본값 설정 REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = os.getenv("REDIS_PORT", 6379) diff --git a/App/main.py b/App/main.py index 95c3c97..d0cf85e 100644 --- a/App/main.py +++ b/App/main.py @@ -1,21 +1,6 @@ import os -import sys -import warnings -import logging -from pathlib import Path from dotenv import load_dotenv -# ------ 상위 폴더 경로 설정 -------- -REQUIRED_PACKAGES = ["deepguard", "inference", "explainability", "preprocess"] - -current_file = Path(__file__).resolve() -project_root = current_file.parent.parent - -for pkg in REQUIRED_PACKAGES: - pkg_path = str(project_root / pkg) - if pkg_path not in sys.path: - sys.path.insert(0, pkg_path) - # ------ .env 파일 가져오기 ------ load_dotenv() diff --git a/App/services/inference_svc.py b/App/services/inference_svc.py index 3ca92d6..02a8f60 100644 --- a/App/services/inference_svc.py +++ b/App/services/inference_svc.py @@ -207,7 +207,7 @@ async def run_inference(): if result["analysis"].get("frame_results"): try: async with celery_db_conn() as conn: - await video_svc.save_video_frame_results( + await video_svc.save_video_frame_result( conn, video_id, result["analysis"]["frame_results"] ) except Exception as e: diff --git a/App/services/video_svc.py b/App/services/video_svc.py index cdf5ac4..4455e1d 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -347,7 +347,7 @@ async def save_video_meta_result(conn: Connection, video_id: int, analysis: dict # 비디오 상세 결과 값 저장하기 # 비디오 프레임 별 딥페이크 점수, 얼굴 신뢰도, 얼굴 비율, 얼굴 조도 반환 -async def save_video_frame_results(conn: Connection, video_id: int, frame_results: list): +async def save_video_frame_result(conn: Connection, video_id: int, frame_results: list): try: query = """ INSERT INTO video_frame_result diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2d4ad22 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,51 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +# ───────────────────────────────────────────── +# [project] +# PyPI에 올라가는 패키지 메타데이터 +# pip install deepguard 했을 때 보이는 정보들 +# ───────────────────────────────────────────── +[project] +name = "deepguard" # pip install deepguard 할 때 이 이름 +version = "0.2.1" # pip install deepguard==0.2.1 으로 버전 고정 가능 +description = "Multi-Scale Efficient Vision Transformer for Robust Deepfake Detection" +readme = "README.md" # PyPI 페이지에 표시될 설명 (마크다운) +requires-python = ">=3.10" # 이 버전 미만이면 pip install 자체를 막음 +license = { text = "MIT" } +authors = [ + { name = "seoyunje", email = "seoyunje2001@gmail.com" } +] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] + +dependencies = ["torch>=2.10.0", + "torchmetrics>=1.6.1", + "timm>=1.0.12", + "ultralytics>=8.4.41", + "grad-cam>=1.5.5" + ] + +[project.urls] +Homepage = "https://github.com/HanMoonSub/DeepGuard" +"Bug Tracker" = "https://github.com/HanMoonSub/DeepGuard/issues" +"Source Code" = "https://github.com/HanMoonSub/DeepGuard" + + +[tool.setuptools.packages.find] + +include = [ + "deepguard*", # 모델, 레이어, 데이터셋 등 핵심 패키지 + "inference*", # ImagePredictor, VideoPredictor + "explainability*", # CAM 기반 XAI + "preprocess*", # 얼굴 검출, 프레임 추출 등 +] + +exclude = ["App*", "client*"] \ No newline at end of file From 27066ad1f566da0756de6346e7f91e4ae146a701 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 14 May 2026 10:58:27 +0900 Subject: [PATCH 23/48] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20Cam=20Router=20->?= =?UTF-8?q?=20Explain=20Router?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/main.py | 4 ++-- App/routes/cam.py | 3 --- App/routes/explain.py | 5 +++++ 3 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 App/routes/cam.py create mode 100644 App/routes/explain.py diff --git a/App/main.py b/App/main.py index d0cf85e..14fafc2 100644 --- a/App/main.py +++ b/App/main.py @@ -16,7 +16,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from fastapi.middleware.cors import CORSMiddleware # from starlette.middleware.sessions import SessionMiddleware -from routes import auth, home, inference, image, video, cam +from routes import auth, home, inference, image, video, explain from utils import exc_handler, middleware, common @@ -51,7 +51,7 @@ app.include_router(inference.router) # 이미지, 비디오 비동기 추론 접수, 추론 값 가져오기 app.include_router(image.router) # 이미지 삭제 및 가져오기 app.include_router(video.router) # 비디오 삭제 및 가져오기 -app.include_router(cam.router) # 딥페이크 이미지, 비디오 위조 흔적 표시 +app.include_router(explain.router) # 딥페이크 이미지, 비디오 위조 흔적 표시 # 커스텀 예외 처리: HTTPException app.add_exception_handler(StarletteHTTPException, exc_handler.custom_http_exception_handler) diff --git a/App/routes/cam.py b/App/routes/cam.py deleted file mode 100644 index eb0a78c..0000000 --- a/App/routes/cam.py +++ /dev/null @@ -1,3 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter(prefix="/cam", tags=["cam"]) \ No newline at end of file diff --git a/App/routes/explain.py b/App/routes/explain.py new file mode 100644 index 0000000..f012bc9 --- /dev/null +++ b/App/routes/explain.py @@ -0,0 +1,5 @@ +from fastapi import APIRouter, status +from sqlalchemy import Connection +from db.database import context_get_conn + +router = APIRouter(prefix="/explain", tags=["explain"]) From 573ab4b90369a3a429e7751098e8871ecdafe52c Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 14 May 2026 11:12:30 +0900 Subject: [PATCH 24/48] =?UTF-8?q?=E2=9C=A8=20Create=20explain=5Fimage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/App/routes/explain.py b/App/routes/explain.py index f012bc9..7aba577 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -1,5 +1,22 @@ -from fastapi import APIRouter, status +import io +from fastapi import APIRouter, status, Depends +from fastapi.responses import StreamingResponse from sqlalchemy import Connection from db.database import context_get_conn +from schemas.explain_schema import ExplainRequest +from services import session_svc, explain_svc router = APIRouter(prefix="/explain", tags=["explain"]) + +@router.post("/image/{image_id}", status_code=status.HTTP_200_OK, + response_class=StreamingResponse , summary="딥페이크 이미지 위조 흔적 시각화(CAM)") +async def explain_image( + image_id: int, + explain_req: ExplainRequest, + conn: Connection = Depends(context_get_conn), + session_user = Depends(session_svc.get_session_user_opt), # 비회원 허용 +): + img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) + return StreamingResponse(io.BytesIO(img_bytes), media_type="image/png") + + \ No newline at end of file From 00a8416bd73f7f6a6d2be560a6df57e2eaa59aea Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 14 May 2026 11:39:54 +0900 Subject: [PATCH 25/48] =?UTF-8?q?=F0=9F=8E=A8=20Add=20response=5Fclass=20&?= =?UTF-8?q?=20response=5Fmodel=20in=20inference=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/inference.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/App/routes/inference.py b/App/routes/inference.py index ba856af..2afff51 100644 --- a/App/routes/inference.py +++ b/App/routes/inference.py @@ -1,17 +1,19 @@ from fastapi import ( - APIRouter, Depends, status, Form, - File, UploadFile -) + APIRouter, UploadFile, status, + Depends, Form, File) +from fastapi.responses import JSONResponse from fastapi.exceptions import HTTPException from services import image_svc, session_svc, inference_svc, video_svc from sqlalchemy import Connection from db.database import context_get_conn -from schemas.video_schema import VideoDetailResponse +from schemas.video_schema import VideoDetailResponse, VideoDetailData +from schemas.image_schema import ImageData_indi router = APIRouter(prefix="/inference", tags=["inference"]) -@router.post("/image", status_code=status.HTTP_202_ACCEPTED, summary="딥페이크 비동기 이미지 추론 접수") +@router.post("/image", status_code=status.HTTP_202_ACCEPTED, + response_class=JSONResponse, summary="딥페이크 비동기 이미지 추론 접수") async def predict_image( imagefile: UploadFile = File(...), # 사용자가 업로드한 이미지 객체 version_type: str = Form(...), # deepguard1, deepguard2 @@ -43,7 +45,8 @@ async def predict_image( "status": "success", } -@router.get("/image/{image_id}", status_code=status.HTTP_200_OK, summary="딥페이크 비동기 이미지 추론 결과값 가져오기") +@router.get("/image/{image_id}", status_code=status.HTTP_200_OK, + response_model=ImageData_indi, summary="딥페이크 비동기 이미지 추론 결과값 가져오기") async def get_image_result( image_id: int, conn: Connection = Depends(context_get_conn), @@ -62,7 +65,8 @@ async def get_image_result( ) return image_data -@router.post("/video", status_code=status.HTTP_202_ACCEPTED, summary="딥페이크 비동기 비디오 추론 접수") +@router.post("/video", status_code=status.HTTP_202_ACCEPTED, + response_class=JSONResponse, summary="딥페이크 비동기 비디오 추론 접수") async def predict_video( videofile: UploadFile = File(...), # 사용자가 업로드한 비디오 객체 version_type: str = Form(...), # deepguard1, deepguard2 @@ -94,7 +98,8 @@ async def predict_video( "status": "success", } -@router.get("/video/{video_id}", status_code=status.HTTP_200_OK, summary="딥페이크 비디오 비디오 추론 결과값 가져오기") +@router.get("/video/{video_id}", status_code=status.HTTP_200_OK, + response_model=VideoDetailData, summary="딥페이크 비디오 비디오 추론 결과값 가져오기") async def get_video_result( video_id: int, conn: Connection = Depends(context_get_conn), @@ -117,7 +122,8 @@ async def get_video_result( return video_data -@router.get("/video/{video_id}/detail", status_code=status.HTTP_200_OK, summary="딥페이크 상세 분석 결과값 가져오기") +@router.get("/video/{video_id}/detail", status_code=status.HTTP_200_OK, + response_model=VideoDetailResponse ,summary="딥페이크 상세 분석 결과값 가져오기") async def get_video_detail( video_id: int, conn: Connection = Depends(context_get_conn), From 302aca4be6aa255db1df8d1622f3d57ed314554c Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 14:38:08 +0900 Subject: [PATCH 26/48] =?UTF-8?q?=F0=9F=93=9D=20Add=20python-dotenv=20&=20?= =?UTF-8?q?cryptography?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 72e2059..a7bc9ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ python-multipart>=0.0.22 pydantic[email]>=2.12.5 aiofiles>=25.1.0 itsdangerous>=2.2.0 +python-dotenv>=0.9.9 # --- [Database & ORM] --- sqlalchemy>=2.0.48 @@ -16,6 +17,7 @@ redis>=7.4.0 # --- [Security & Auth] --- bcrypt>=4.0.1 passlib[bcrypt]>=1.7.4 +cryptography>=42.0.0 # --- [Data Analysis & Math] --- numpy>=1.23.0,<3.0.0 From c8e5566645ff7b13428618fa509ac1a0b5d3cc6a Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 14:45:11 +0900 Subject: [PATCH 27/48] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20debugging?= =?UTF-8?q?=20print=20in=20upload=5Fvideo,=20delete=5Fvideo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/video_svc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/App/services/video_svc.py b/App/services/video_svc.py index 4455e1d..fd69f20 100644 --- a/App/services/video_svc.py +++ b/App/services/video_svc.py @@ -52,8 +52,6 @@ async def upload_video(user_email: str | None, videofile: UploadFile) -> str: detail="비디오 파일을 저장하는 중 오류가 발생했습니다." ) - print(f"Upload Succeeded: {upload_video_loc}") - # 6. DB 저장용 경로 반환 return upload_video_loc[1:].replace("\\", "/") @@ -73,7 +71,6 @@ async def delete_video(video_loc: str): if os.path.exists(file_path): os.remove(file_path) - print(f"Video file removed: {file_path}") else: print(f"Video file not found: {file_path}") From b430278d61d254a4583df3a426ea90e1345fc9ee Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 14:55:43 +0900 Subject: [PATCH 28/48] =?UTF-8?q?=E2=9C=A8=20Add=20validation=20and=20desc?= =?UTF-8?q?ription=20to=20image,=20video=20inference=20params?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/inference.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/App/routes/inference.py b/App/routes/inference.py index 2afff51..b538ebb 100644 --- a/App/routes/inference.py +++ b/App/routes/inference.py @@ -8,7 +8,7 @@ from db.database import context_get_conn from schemas.video_schema import VideoDetailResponse, VideoDetailData from schemas.image_schema import ImageData_indi - +from typing import Literal router = APIRouter(prefix="/inference", tags=["inference"]) @@ -16,9 +16,9 @@ response_class=JSONResponse, summary="딥페이크 비동기 이미지 추론 접수") async def predict_image( imagefile: UploadFile = File(...), # 사용자가 업로드한 이미지 객체 - version_type: str = Form(...), # deepguard1, deepguard2 - model_type: str = Form(...), # fast model, pro moel - domain_type: str = Form(...), # model 학습시 사용한 dataset 종류 + version_type: Literal["v1","v2"] = Form("v2", description="모델 엔진 버전"), + model_type: Literal["fast","pro"] = Form("fast", description="추론 모드 (fast: 속도 우선, pro: 정확도 우선)"), + domain_type: Literal["서양인","동양인"] = Form("서양인", description="학습 데이터셋 도메인"), conn: Connection = Depends(context_get_conn), # 이미지 File 저장 session_user = Depends(session_svc.get_session_user_opt) # Signed Cookie 없을 시 None 반환 ): @@ -69,9 +69,9 @@ async def get_image_result( response_class=JSONResponse, summary="딥페이크 비동기 비디오 추론 접수") async def predict_video( videofile: UploadFile = File(...), # 사용자가 업로드한 비디오 객체 - version_type: str = Form(...), # deepguard1, deepguard2 - model_type: str = Form(...), # fast model, pro moel - domain_type: str = Form(...), # model 학습시 사용한 dataset 종류 + version_type: Literal["v1","v2"] = Form("v2", description="모델 엔진 버전"), + model_type: Literal["fast","pro"] = Form("fast", description="추론 모드 (fast: 속도 우선, pro: 정확도 우선)"), + domain_type: Literal["서양인","동양인"] = Form("서양인", description="학습 데이터셋 도메인"), conn: Connection = Depends(context_get_conn), # 비디오 File 저장 session_user = Depends(session_svc.get_session_user_opt) # Signed Cookie 없을 시 None 반환 ): From 13bb4c6ce6c87f3f3ec73bb3e6212e8440fe52a2 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 15:13:00 +0900 Subject: [PATCH 29/48] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20Debugging?= =?UTF-8?q?=20print=20upload=5Fimage,=20delete=5Fimage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/image_svc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/App/services/image_svc.py b/App/services/image_svc.py index 9a92d68..e094f6f 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -53,8 +53,6 @@ async def upload_image(user_email: str | None, imagefile: UploadFile) -> str: detail="이미지 파일을 저장하는 중 오류가 발생했습니다." ) - print(f"Upload Succeeded: {upload_image_loc}") - # 6. DB 저장용 경로 반환 return upload_image_loc[1:].replace("\\", "/") @@ -75,7 +73,6 @@ async def delete_image(image_loc: str): if os.path.exists(file_path): os.remove(file_path) - print(f"File removed: {file_path}") else: print(f"File not found: {file_path}") From da1ab758db21c86f6b924b855dc8f2f75b5acece Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 16:17:57 +0900 Subject: [PATCH 30/48] =?UTF-8?q?=E2=9C=A8=20Create=20ExplainRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 7 +++-- App/schemas/explain_schema.py | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 App/schemas/explain_schema.py diff --git a/App/routes/explain.py b/App/routes/explain.py index 7aba577..3d35db9 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -4,7 +4,7 @@ from sqlalchemy import Connection from db.database import context_get_conn from schemas.explain_schema import ExplainRequest -from services import session_svc, explain_svc +from services import session_svc # explain_svc router = APIRouter(prefix="/explain", tags=["explain"]) @@ -16,7 +16,8 @@ async def explain_image( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_opt), # 비회원 허용 ): - img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) - return StreamingResponse(io.BytesIO(img_bytes), media_type="image/png") + return None +# img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) +# return StreamingResponse(io.BytesIO(img_bytes), media_type="image/png") \ No newline at end of file diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py new file mode 100644 index 0000000..8ad5823 --- /dev/null +++ b/App/schemas/explain_schema.py @@ -0,0 +1,58 @@ +from typing import Literal +from pydantic import BaseModel, Field, model_validator + +# ── Branch별 허용 기법 ──────────────────────────────────── +# +# [LOW branch] 고해상도 feature map → 국소 위조 흔적 포착에 특화 +# - hirescam : activation * gradient element-wise 곱, 업샘플링 없이 원본 해상도 유지 +# → 픽셀 단위 경계가 가장 선명 (aug_smooth: O, eigen_smooth: X) +# - gradcamelementwise : 각 위치별 gradient 부호 고려 후 ReLU → 노이즈 억제, 국소 영역 정밀 (aug_smooth: O, eigen_smooth: O) +# - layercam : positive gradient로 activation 공간 가중합산 +# → 얕은 레이어에서도 안정적, 텍스처/경계 검출에 강함 (aug_smooth: O, eigen_smooth: O) +# +# [HIGH branch] 저해상도 semantic feature map → 전체 구조 파악에 특화 +# - eigengradcam : activation * gradient 의 첫 번째 주성분 +# → 클래스 판별력 유지하면서 GradCAM보다 부드럽고 EigenCAM보다 정확 (aug_smooth: O, eigen_smooth: X) +# - gradcamplusplus : 2차 gradient 활용 → 다중 위조 영역 동시 포착, 작은 영역에 강함 (aug_smooth: O, eigen_smooth: X) +# - xgradcam : gradient를 normalized activation으로 스케일링 +# → attribution 합이 logit 변화량과 수학적으로 일치, 충실도 높음 (aug_smooth: O, eigen_smooth: O) + +_LOW_ALLOWED = {"hirescam", "gradcamelementwise", "layercam"} +_HIGH_ALLOWED = {"eigengradcam", "gradcamplusplus", "xgradcam"} + +class ExplainRequest(BaseModel): + branch_level: Literal["low","high"] = Field("high", + description="브랜치 레벨. low: 국소 위조 흔적 포착, high: 전역적 위조 흔적 포착") + explainer_type: str = Field("eigengradcam", + description = ( + "선택 가능한 XAI 기법. " + "low: [hirescam, gradcamelementwise, layercam], " + "high: [eigengradcam, gradcamplusplus, xgradcam]" + )) + display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", + description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") + category: Literal[0, 1] = Field(1, + description="판단 클래스 인덱스 (0: Real / 1: Fake)") + overlay_ratio: float = Field( + 0.5, ge=0.0, le=1.0, + description = "Heatmap 투명도 (0: 히트맵만 강조, 1: 원본 이미지 위주)" + ) + aug_smooth: bool = Field( + False, + description = "TTA(Test Time Augmentation) 적용 여부. 히트맵을 더 객체 중심적으로 정렬" + ) + eigen_smooth: bool = Field( + False, + description = "PCA 기반 노이즈 제거. 지배적인 패턴만 남김" + ) + + @model_validator(mode="after") + def validate_explainer_for_branch(self) -> "ExplainRequest": + allowed = {"low": _LOW_ALLOWED, "high": _HIGH_ALLOWED} + if self.explainer_type not in allowed[self.branch_level]: + raise ValueError( + f"branch_level='{self.branch_level}'에서 허용된 기법: " + f"{sorted(allowed[self.branch_level])} " + f"(입력값: '{self.explainer_type}')" + ) + return self \ No newline at end of file From 086af0025600b310ef4d266a506cad15eef3c459 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 16:30:03 +0900 Subject: [PATCH 31/48] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20BinaryCla?= =?UTF-8?q?ssifierOutputSigmoidTarget=20with=20removing=20category?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/cam_evaluator.py | 2 +- explainability/utils/model_targets.py | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/explainability/cam_evaluator.py b/explainability/cam_evaluator.py index c325811..d215def 100644 --- a/explainability/cam_evaluator.py +++ b/explainability/cam_evaluator.py @@ -32,7 +32,7 @@ def evaluate(self, img_path: str) -> float: np.random.seed(self.seed) grayscale_cam, tensor, _ = self.cam_explainer._build_grayscale_cam(img_path) - targets = [BinaryClassifierOutputSigmoidTarget(self.cam_explainer.category)] + targets = [BinaryClassifierOutputSigmoidTarget()] return self.cam_metric( input_tensor = tensor, # (1,C,H,W) diff --git a/explainability/utils/model_targets.py b/explainability/utils/model_targets.py index 5ec0870..ba63a85 100644 --- a/explainability/utils/model_targets.py +++ b/explainability/utils/model_targets.py @@ -12,13 +12,5 @@ def __call__(self, model_output): return model_output * sign class BinaryClassifierOutputSigmoidTarget: - def __init__(self, category): - self.category = category - def __call__(self, model_output): - if self.category == 1: - sign = 1 - else: - sign = -1 - return torch.sigmoid(model_output) * sign - \ No newline at end of file + return torch.sigmoid(model_output) \ No newline at end of file From b6bfd16659fa790b52100a18c4d5373ed7134a97 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Fri, 15 May 2026 18:14:16 +0900 Subject: [PATCH 32/48] =?UTF-8?q?=E2=9C=A8=20Create=20PNGStreamingResponse?= =?UTF-8?q?(media=5Ftype=3D"image/png")?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/utils/responses.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 App/utils/responses.py diff --git a/App/utils/responses.py b/App/utils/responses.py new file mode 100644 index 0000000..4647454 --- /dev/null +++ b/App/utils/responses.py @@ -0,0 +1,4 @@ +from fastapi.responses import StreamingResponse + +class PNGStreamingResponse(StreamingResponse): + media_type = "image/png" \ No newline at end of file From dd81bba64050582ff677f36f11c219e52448751a Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 16 May 2026 13:38:18 +0900 Subject: [PATCH 33/48] =?UTF-8?q?=E2=9C=A8=20Create=20explain=5Fsvc.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 13 +++--- App/routes/inference.py | 9 +++- App/schemas/explain_schema.py | 31 ++++---------- App/services/explain_svc.py | 79 +++++++++++++++++++++++++++++++++++ App/services/inference_svc.py | 78 +++++++++++++++++----------------- 5 files changed, 140 insertions(+), 70 deletions(-) create mode 100644 App/services/explain_svc.py diff --git a/App/routes/explain.py b/App/routes/explain.py index 3d35db9..ea1b1e9 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -1,23 +1,22 @@ import io from fastapi import APIRouter, status, Depends -from fastapi.responses import StreamingResponse +from utils.responses import PNGStreamingResponse from sqlalchemy import Connection from db.database import context_get_conn from schemas.explain_schema import ExplainRequest -from services import session_svc # explain_svc +from services import session_svc, explain_svc router = APIRouter(prefix="/explain", tags=["explain"]) @router.post("/image/{image_id}", status_code=status.HTTP_200_OK, - response_class=StreamingResponse , summary="딥페이크 이미지 위조 흔적 시각화(CAM)") + response_class=PNGStreamingResponse , summary="딥페이크 이미지 위조 흔적 시각화(CAM)") async def explain_image( image_id: int, explain_req: ExplainRequest, conn: Connection = Depends(context_get_conn), - session_user = Depends(session_svc.get_session_user_opt), # 비회원 허용 + session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 ): - return None -# img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) -# return StreamingResponse(io.BytesIO(img_bytes), media_type="image/png") + img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) + return PNGStreamingResponse(io.BytesIO(img_bytes)) \ No newline at end of file diff --git a/App/routes/inference.py b/App/routes/inference.py index b538ebb..9d04a32 100644 --- a/App/routes/inference.py +++ b/App/routes/inference.py @@ -127,8 +127,15 @@ async def get_video_result( async def get_video_detail( video_id: int, conn: Connection = Depends(context_get_conn), - session_user = Depends(session_svc.get_session_user_prt) # 로그인 유저만 가능 + session_user = Depends(session_svc.get_session_user_prt) # 로그인 필수 ): + video_data = await video_svc.get_video_result(conn, video_id) + if video_data.status != "SUCCESS": + raise HTTPException( + status_code = status.HTTP_400_BAD_REQUEST, + detail = f"비디오 상세 분석은 추론이 성공한 비디오만 가능합니다" + ) + meta = await video_svc.get_video_meta_result(conn, video_id) frames = await video_svc.get_video_frame_results(conn, video_id) diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py index 8ad5823..58c8b86 100644 --- a/App/schemas/explain_schema.py +++ b/App/schemas/explain_schema.py @@ -21,30 +21,13 @@ _HIGH_ALLOWED = {"eigengradcam", "gradcamplusplus", "xgradcam"} class ExplainRequest(BaseModel): - branch_level: Literal["low","high"] = Field("high", - description="브랜치 레벨. low: 국소 위조 흔적 포착, high: 전역적 위조 흔적 포착") - explainer_type: str = Field("eigengradcam", - description = ( - "선택 가능한 XAI 기법. " - "low: [hirescam, gradcamelementwise, layercam], " - "high: [eigengradcam, gradcamplusplus, xgradcam]" - )) - display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", - description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") - category: Literal[0, 1] = Field(1, - description="판단 클래스 인덱스 (0: Real / 1: Fake)") - overlay_ratio: float = Field( - 0.5, ge=0.0, le=1.0, - description = "Heatmap 투명도 (0: 히트맵만 강조, 1: 원본 이미지 위주)" - ) - aug_smooth: bool = Field( - False, - description = "TTA(Test Time Augmentation) 적용 여부. 히트맵을 더 객체 중심적으로 정렬" - ) - eigen_smooth: bool = Field( - False, - description = "PCA 기반 노이즈 제거. 지배적인 패턴만 남김" - ) + branch_level: Literal["low","high"] = Field("high", description="브랜치 레벨\nlow: 국소 위조 흔적 포착\nhigh: 전역적 위조 흔적 포착") + explainer_type: str = Field("eigengradcam", description = ("선택 가능한 XAI 기법. low: [hirescam, gradcamelementwise, layercam], ""high: [eigengradcam, gradcamplusplus, xgradcam]")) + display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") + category: Literal[0, 1] = Field(1, description="판단 클래스 인덱스 (0: Real / 1: Fake)") + overlay_ratio: float = Field(0.5, ge=0.0, le=1.0, description = "Heatmap 투명도 (0: 히트맵만 강조, 1: 원본 이미지 위주)") + aug_smooth: bool = Field(False, description = "TTA(Test Time Augmentation) 적용 여부. 히트맵을 더 객체 중심적으로 정렬") + eigen_smooth: bool = Field(False, description = "PCA 기반 노이즈 제거. 지배적인 패턴만 남김") @model_validator(mode="after") def validate_explainer_for_branch(self) -> "ExplainRequest": diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py new file mode 100644 index 0000000..9ad6f7d --- /dev/null +++ b/App/services/explain_svc.py @@ -0,0 +1,79 @@ +import cv2 +import asyncio +from functools import partial +from fastapi import status +from fastapi.exceptions import HTTPException +from sqlalchemy import Connection +from schemas.explain_schema import ExplainRequest +from services import image_svc, inference_svc +from explainability import ( + HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer, + EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer, +) + +EXPLAINER_REGISTRY = { + # LOW branch: 국소 위조 흔적 포착 + "hirescam": HiResCAMExplainer, + "gradcamelementwise": GradCAMElementWiseExplainer, + "layercam": LayerCAMExplainer, + # HIGH branch: 전역 위조 흔적 포착 + "eigengradcam": EigenGradCAMExplainer, + "gradcamplusplus": GradCAMPlusPlusExplainer, + "xgradcam": XGradCAMExplainer, +} + +_explainer_cache: dict = {} + +def _get_or_create_explainer(model_name: str, dataset: str, explain_req: ExplainRequest): + cache_key = (model_name, dataset, + explain_req.explainer_type, explain_req.branch_level, + explain_req.category, explain_req.aug_smooth, explain_req.eigen_smooth) + + if cache_key not in _explainer_cache: + _explainer_cache[cache_key] = EXPLAINER_REGISTRY[explain_req.explainer_type]( + model_name = model_name, + dataset = dataset, + branch_level = explain_req.branch_level, + category = explain_req.category, + aug_smooth = explain_req.aug_smooth, + eigen_smooth = explain_req.eigen_smooth, + ) + return _explainer_cache[cache_key] + +def _run_visualization(explainer, image_path: str, explain_req: ExplainRequest) -> bytes: + if explain_req.display_type == "heatmap": + img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio) + elif explain_req.display_type == "contour": + img = explainer.display_contour_on_image(image_path) + else: + img = explainer.display_bbox_on_image(image_path) + + img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + _, buf = cv2.imencode(".png", img_bgr) + return buf.tobytes() + +async def explain_image(conn: Connection, image_id: int, explain_req: ExplainRequest): + result = await image_svc.get_image_result(conn, image_id) + + if result.status != "SUCCESS": + raise HTTPException( + status_code = status.HTTP_400_BAD_REQUEST, + detail = "이미지 위조 흔적 분석은 추론이 성공한 이미지만 가능합니다" + ) + + model_name, dataset = inference_svc.MODEL_CONFIG[result.version_type][result.model_type][result.domain_type] + + explainer = _get_or_create_explainer(model_name, dataset, explain_req) + image_path = "." + result.image_loc + + try: + loop = asyncio.get_event_loop() + return await loop.run_in_executor( + None, + partial(_run_visualization, explainer, image_path, explain_req) + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail="알수없는 이유로 문제가 발생하였습니다." + ) diff --git a/App/services/inference_svc.py b/App/services/inference_svc.py index 02a8f60..1a1d440 100644 --- a/App/services/inference_svc.py +++ b/App/services/inference_svc.py @@ -1,17 +1,15 @@ import asyncio -from inference.image_predictor_prt import ImagePredictor -from inference.video_predictor_prt import VideoPredictor -from inference.utils import PredictorError +from typing import Literal from fastapi import status, Depends from fastapi.exceptions import HTTPException from sqlalchemy import text, Connection from sqlalchemy.exc import SQLAlchemyError from services import image_svc, video_svc -from db.database import context_get_conn, celery_db_conn, engine +from db.database import celery_db_conn from celery_app import celery_app - -image_cache = {} -video_cache = {} +from inference.image_predictor_prt import ImagePredictor +from inference.video_predictor_prt import VideoPredictor +from inference.utils import PredictorError # 사용자 모델 설정 변수명 MODEL_CONFIG = { @@ -31,29 +29,46 @@ } } -# 사용자 이미지 딥페이크 여부 판단 로직 -def predict_image(image_loc: str, version_type: str, model_type: str, domain_type: str): - - # image_loc: static 폴더 내 저장된 사용자 이미지 경로 - # version_type: v1, v2 - # model_type: fast, pro - # domain_type: 서양인, 동양인 - - model_name, dataset = MODEL_CONFIG[version_type][model_type][domain_type] +_image_cache: dict = {} +_video_cache: dict = {} - # 캐시 확인 및 모델 초기화 + # 캐시 확인 및 모델 초기화 +def _get_or_create_predictor( + model_name: str, + dataset: str, + predictor_mode: Literal["image", "video"], +) -> ImagePredictor | VideoPredictor: cache_key = (model_name, dataset) - if cache_key not in image_cache: - image_cache[cache_key] = ImagePredictor( + + if predictor_mode == "image": + if cache_key not in _image_cache: + _image_cache[cache_key] = ImagePredictor( + margin_ratio=0.2, + conf_thres=0.5, + model_name=model_name, + dataset=dataset, + ) + return _image_cache[cache_key] + + if cache_key not in _video_cache: + _video_cache[cache_key] = VideoPredictor( margin_ratio=0.2, conf_thres=0.5, model_name=model_name, - dataset=dataset + dataset=dataset, ) - predictor = image_cache[cache_key] + return _video_cache[cache_key] + +# 사용자 이미지 딥페이크 여부 판단 로직 +def predict_image(image_loc: str, version_type: str, model_type: str, domain_type: str): + + model_name, dataset = MODEL_CONFIG[version_type][model_type][domain_type] + + predictor = _get_or_create_predictor(model_name, dataset, "image") + image_path = "." + image_loc try: - analysis = predictor.predict_img("." + image_loc) + analysis = predictor.predict_img(image_path) print(f"딥페이크 확률 값: {analysis['prob']}, 얼굴 신뢰도: {analysis['face_conf']}, 얼굴 비율: {analysis['face_ratio']}, 얼굴 밝기: {analysis['face_brightness']}") @@ -130,26 +145,13 @@ async def run_inference(): # 사용자 비디오 딥페이크 여부 판단 로직 def predict_video(video_loc: str, version_type: str, model_type: str, domain_type: str): - # video_loc: static 폴더 내 저장된 사용자 비디오 경로 - # version_type: v1, v2 - # model_type: fast, pro - # domain_type: 서양인, 동양인 - model_name, dataset = MODEL_CONFIG[version_type][model_type][domain_type] - # 캐시 확인 및 모델 초기화 - cache_key = (model_name, dataset) - if cache_key not in video_cache: - video_cache[cache_key] = VideoPredictor( - margin_ratio=0.2, - conf_thres=0.5, - model_name=model_name, - dataset=dataset - ) - predictor = video_cache[cache_key] + predictor = _get_or_create_predictor(model_name, dataset, "video") + video_path = "." + video_loc try: - analysis = predictor.predict_video("." + video_loc) + analysis = predictor.predict_video(video_path) print(f"딥페이크 확률 값: {analysis['prob']}, 얼굴 신뢰도: {analysis['face_conf']}, 얼굴 비율: {analysis['face_ratio']}, 얼굴 밝기: {analysis['face_brightness']}") From bc55e95038f696b5048eaa2301ca7b440f0a77a8 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 16 May 2026 14:04:54 +0900 Subject: [PATCH 34/48] =?UTF-8?q?=F0=9F=8E=A8=20Add=20threshold=20argument?= =?UTF-8?q?=20in=20ExplainRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 15 ++++++++++++--- App/schemas/explain_schema.py | 1 + App/services/explain_svc.py | 16 +++++----------- explainability/explainer/cam_explainer.py | 7 ++++++- 4 files changed, 24 insertions(+), 15 deletions(-) diff --git a/App/routes/explain.py b/App/routes/explain.py index ea1b1e9..d15a71d 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -1,10 +1,11 @@ import io from fastapi import APIRouter, status, Depends +from fastapi.exceptions import HTTPException from utils.responses import PNGStreamingResponse from sqlalchemy import Connection from db.database import context_get_conn from schemas.explain_schema import ExplainRequest -from services import session_svc, explain_svc +from services import session_svc, explain_svc, image_svc router = APIRouter(prefix="/explain", tags=["explain"]) @@ -16,7 +17,15 @@ async def explain_image( conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 ): - img_bytes = await explain_svc.explain_image(conn, image_id, explain_req) - return PNGStreamingResponse(io.BytesIO(img_bytes)) + result = await image_svc.get_image_result(conn, image_id) + + if result.status != "SUCCESS": + raise HTTPException( + status_code = status.HTTP_400_BAD_REQUEST, + detail = "이미지 위조 흔적 분석은 추론이 성공한 이미지만 가능합니다" + ) + + img_bytes = await explain_svc.explain_image(conn, result, explain_req) + return PNGStreamingResponse(io.BytesIO(img_bytes)) \ No newline at end of file diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py index 58c8b86..b730a51 100644 --- a/App/schemas/explain_schema.py +++ b/App/schemas/explain_schema.py @@ -26,6 +26,7 @@ class ExplainRequest(BaseModel): display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") category: Literal[0, 1] = Field(1, description="판단 클래스 인덱스 (0: Real / 1: Fake)") overlay_ratio: float = Field(0.5, ge=0.0, le=1.0, description = "Heatmap 투명도 (0: 히트맵만 강조, 1: 원본 이미지 위주)") + threshold: float = Field(0.5, ge=0.5, le=1.0, description="contour/bbox 이진화 임계값 (0.0~1.0)") aug_smooth: bool = Field(False, description = "TTA(Test Time Augmentation) 적용 여부. 히트맵을 더 객체 중심적으로 정렬") eigen_smooth: bool = Field(False, description = "PCA 기반 노이즈 제거. 지배적인 패턴만 남김") diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py index 9ad6f7d..6bd0e95 100644 --- a/App/services/explain_svc.py +++ b/App/services/explain_svc.py @@ -5,6 +5,7 @@ from fastapi.exceptions import HTTPException from sqlalchemy import Connection from schemas.explain_schema import ExplainRequest +from schemas.image_schema import ImageData_indi from services import image_svc, inference_svc from explainability import ( HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer, @@ -42,24 +43,17 @@ def _get_or_create_explainer(model_name: str, dataset: str, explain_req: Explain def _run_visualization(explainer, image_path: str, explain_req: ExplainRequest) -> bytes: if explain_req.display_type == "heatmap": - img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio) + img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio, threshold=explain_req.threshold) elif explain_req.display_type == "contour": - img = explainer.display_contour_on_image(image_path) + img = explainer.display_contour_on_image(image_path, threshold=explain_req.threshold) else: - img = explainer.display_bbox_on_image(image_path) + img = explainer.display_bbox_on_image(image_path, threshold=explain_req.threshold) img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) _, buf = cv2.imencode(".png", img_bgr) return buf.tobytes() -async def explain_image(conn: Connection, image_id: int, explain_req: ExplainRequest): - result = await image_svc.get_image_result(conn, image_id) - - if result.status != "SUCCESS": - raise HTTPException( - status_code = status.HTTP_400_BAD_REQUEST, - detail = "이미지 위조 흔적 분석은 추론이 성공한 이미지만 가능합니다" - ) +async def explain_image(conn: Connection, result: ImageData_indi, explain_req: ExplainRequest): model_name, dataset = inference_svc.MODEL_CONFIG[result.version_type][result.model_type][result.domain_type] diff --git a/explainability/explainer/cam_explainer.py b/explainability/explainer/cam_explainer.py index 5764ef3..36ea75b 100644 --- a/explainability/explainer/cam_explainer.py +++ b/explainability/explainer/cam_explainer.py @@ -78,9 +78,14 @@ def _get_binary_mask(self, cam: np.ndarray, threshold: float | str): def display_heatmap_on_image(self, img_path: str, colormap: int = cv2.COLORMAP_JET, - image_weight: float = 0.5): + image_weight: float = 0.5, + threshold: float | str = "auto" + ): cam_recovered, face = self._prepare_cam(img_path) + binary_mask = self._get_binary_mask(cam_recovered, threshold) + cam_recovered = np.where(binary_mask > 0, cam_recovered, 0.0) + heatmap = show_cam_on_image( np.float32(face) / 255.0, cam_recovered, From 1b12a809ea98cc1d45c40853d56317d07c907d06 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 16 May 2026 16:16:47 +0900 Subject: [PATCH 35/48] =?UTF-8?q?=E2=9C=A8=20Add=20DeepFake=20AI=20Explain?= =?UTF-8?q?ability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ms_eff_vit_b0_high_eigengradcam.JPG | Bin 0 -> 9858 bytes .../ms_eff_vit_b0_high_gradcamplusplus.JPG | Bin 0 -> 8904 bytes .../ms_eff_vit_b0_high_xgradcam.JPG | Bin 0 -> 10401 bytes .../ms_eff_vit_b0_low_gradcamelementwise.JPG | Bin 0 -> 12208 bytes .../ms_eff_vit_b0_low_hirescam.JPG | Bin 0 -> 11089 bytes .../ms_eff_vit_b0_low_layercam.JPG | Bin 0 -> 11790 bytes .../ms_eff_vit_b5_high_eigengradcam.JPG | Bin 0 -> 10367 bytes .../ms_eff_vit_b5_high_gradcamplusplus.JPG | Bin 0 -> 11970 bytes .../ms_eff_vit_b5_high_xgradcam.JPG | Bin 0 -> 11378 bytes .../ms_eff_vit_b5_low_gradcamelementwise.JPG | Bin 0 -> 11845 bytes .../ms_eff_vit_b5_low_hirescam.JPG | Bin 0 -> 14089 bytes .../ms_eff_vit_b5_low_layercam.JPG | Bin 0 -> 13648 bytes ...54\235\270_\352\260\200\354\247\234_1.JPG" | Bin 0 -> 14630 bytes README.md | 49 ++++++++++++++++-- 14 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_high_eigengradcam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_high_gradcamplusplus.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_high_xgradcam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_low_gradcamelementwise.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_low_hirescam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b0_low_layercam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_high_eigengradcam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_high_gradcamplusplus.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_high_xgradcam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_low_gradcamelementwise.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_low_hirescam.JPG create mode 100644 Images/deepfake-xai/ms_eff_vit_b5_low_layercam.JPG create mode 100644 "Images/deepfake-xai/\354\204\234\354\226\221\354\235\270_\352\260\200\354\247\234_1.JPG" diff --git a/Images/deepfake-xai/ms_eff_vit_b0_high_eigengradcam.JPG b/Images/deepfake-xai/ms_eff_vit_b0_high_eigengradcam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..cee217c01a14a182b8ab2dccdd7b20cfa6270b89 GIT binary patch literal 9858 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 literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b0_high_xgradcam.JPG b/Images/deepfake-xai/ms_eff_vit_b0_high_xgradcam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..2362109c09b168cc2f1bb88d169a75f735339a98 GIT binary patch literal 10401 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 literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b0_low_layercam.JPG b/Images/deepfake-xai/ms_eff_vit_b0_low_layercam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..983d8f16b19d8dfc9b9a5d01afa37195b813eb7e GIT binary patch literal 11790 zcmeHtcUY6lw{EB^f`}%F6s4DhA|>=9NS78u0)(PeA)yA4A{JCYBGPLpN(n85-USuu zT{==M^p12X$_;z(-`?k*d;U20-t*k&{&6Pxl3DYuS!=!Tn#nx*W=_UVz5y=kYU*eL zC@3fZe&iS6odnEx=sQwLok6R8-&e{Q32Y7&iiCqB3PR?FHMVWz4(NEsb)x*`n+lJH6)y2(A z-cO1959{*e`f0Nmx3VJE-cjCIUGpyuvZTcQS1i81zM{U8qVCvxV&Zaga$+|m#3Up{ z$QmME{%+njej;vOJbxKbckr^qI(v9KySs6o8nm%>_wiQZ_VIDHmv^*rgxcEMLq%+D zB<)4S#qDiGZ0se)MeHRd?PZ|ivJ#Hsw%mWYx3~M-yN3_fweh9c;WE)XA6fX+lMAKt;r*P5zZ+Q7M@liempx zPf_fYjDHj8e{JOdDYldq?d|04PGfh+{^=g>;Q4=-Z5L+yf3;ExvakR$i|dw`OAa{fOk|35qVXOcU(k!Q9qc?zFQ0B!-!(9qD*P@kct zp*?%%%vn07^JI?cK#UjZnOH!qS6M(TSJ{H7Ukv>6oLptSUJTc#lR8* zUC(6K*>Zw#YAz^47g5SN@>VT z^yw=7SD~P!qNbrebM_qFd9p&~MF1rQ6%{2l6%7qFd3~mMNWKrCW}>;oC9X<)8D?{a z+Y=}e6rFLFN3FCGWZeIQSJD>y^c)@Y6_%^4eEb4}LSQMVw2Z8r{4I42O)YI5UAPGX zX=-MUva@$^yyxVM_VV_*@9XCu@a%bTNa%|&Tuf|Sd_rPUGCngan~;;6mtRm;UQt<9 zT~k~4v8lPGwXMCQb6{|2cw}_!^Z57anc2Ddg~g@i&8_X7-M#&v2ZyJ)C;(J{!ulK8 zzr)2u#zjd@O+|g?6c+`hFZrTkqNd>zr@f>KJ7eQ{nOh?0EKn^vqqOlHkEHPrkS(^K zj+s~LJKyFhv_Fvj=YT!^KSK65uzzrk1L&zJ$jPH(0;m9X9xAk<(xyUR^D+mOrh=ta zcEH_{(~f2P?{un4CsOZq1)cyFeUiU#&pS^jX}~H6JBBBCo_sEH@t$H=pRPcDTI&36 zK$5G&tx3L1Y?Xg{ll_)!jAUo^2>=9ZnX%GoyD@$O_{wksXfc&3n0)yl@ZOsiqlzKb zHt6ErJOe)mA(CNwb8l9@fX>egReLJ|aWMYHu1hDC& zYe8@d7!jU%kZS4xW-^(;OE=c`>UY@{hfebs%q%?ZQf@y193xPXZLAf!b5)gXN3Tx( zf+%0~T35R4o*jM~LE}}^RJwcw9b1OC9Ok>He;H!jC)>smd0J%NVvWZhf3P^lys8@k z*+W=*5SaY=HeM?88i;jEf#>S|fuF5w@_hLxfG4N$5U<-%PmW5)Ul+)bB}=^4vxMG9 zmGTw|8}QE1)$e1AhLKQ}gYV8KfWT7)J!O^QGupx@fQ^t-Y_!@l;el=Vg5zKPE$n$p zsR|q_$HENo-+tzQ(PbQYeL3{E*}p_nNX2gU8QLOzq{RI7@=&|CrJErc*l#)vB_{wM z3o-&y#c-}YZ=ru%#tDFR`z70ZejEI9;jUnsrkQt<-atIUU`I-}f?zIp%)h)`O`kk* z?NR74`kc?(nxSZR#m8ljTgwSU8LvDzH0-}%V6NcESZf2ibZO%Ttv8#{ye?fq{0X2f zfRHsUE8KU~{`DAvB(qNB0gUX~Ek>Tw49%0Aywuf$sCJEz*~A?t^}lo;Tg+vVMOP{@ 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%0AQ(L*D`1OJMR^U?4P^bPV2 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 I(!Xc^2OJb)wEzGB literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b5_high_gradcamplusplus.JPG b/Images/deepfake-xai/ms_eff_vit_b5_high_gradcamplusplus.JPG new file mode 100644 index 0000000000000000000000000000000000000000..14a0b754dffb0eb8ab631d5ae8af7f2a14c992a1 GIT binary patch literal 11970 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 literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b5_high_xgradcam.JPG b/Images/deepfake-xai/ms_eff_vit_b5_high_xgradcam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..1965aef96ee4ce7608b8536ab93b4747462cdd14 GIT binary patch literal 11378 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 literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b5_low_gradcamelementwise.JPG b/Images/deepfake-xai/ms_eff_vit_b5_low_gradcamelementwise.JPG new file mode 100644 index 0000000000000000000000000000000000000000..5c878a71c16ca48f55b2445df67353a4f0c3227c GIT binary patch literal 11845 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 literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b5_low_hirescam.JPG b/Images/deepfake-xai/ms_eff_vit_b5_low_hirescam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..568d4b5d902de14322ec8863d6c9c017b82b8eaa GIT binary patch literal 14089 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 I==b~o0YsylPyhe` literal 0 HcmV?d00001 diff --git a/Images/deepfake-xai/ms_eff_vit_b5_low_layercam.JPG b/Images/deepfake-xai/ms_eff_vit_b5_low_layercam.JPG new file mode 100644 index 0000000000000000000000000000000000000000..2225fd2f901409138413da8fcdb2565df93974fd GIT binary patch literal 13648 zcmeIYWmua{8zvmQP^7e^cxiD$aRLOlAi)y|5Tv*S*8-)bSb-F2&=RCrfDqgYrH;Eh z^l5RYI8~ND&%5ut$A0@`zwg^)kNvTe+{w(`bI&#BIoHf}&oy%~bMYB)4W^~51t1|I z0fbzB02g0K=b$0(E&zam0RRL50ImSYNfZF2moW+efCLuj4j{jblU#m{bMyZE184vM z(w;320M%t4?PXf|@&a&dUH<&J`+oz#EWp|Sk_L)rWPkI>bjer#GxC4O>r=6R$H}B5 z0f5xh|HS{pDM{)d8vie+|4}AMaY-3@aj?9E%xwvKNwX2yqfnQKq9&43Q7<^7V2Kw7(tV<>?(D zAELzlckS|*@juyO+{%jnPA>9rO|5^GxJ)T=|En$c@81`_FD2^Z?CB-Bq zMJ`K-1cZ7A+J}gE2k`u>0!`-tM}K$UKzASS+kYywckl@cRN@W_a(9wL?k4f>_zOIq$EU~q@6HRmj#-x0Z2*6$Vkb_C@9D;?K8>4%liOwMvCiq zB-AKxnAlTs`!PvIVhgYEsMmb}!biXIN;&vPQPbRHW?^OH;};MV0!f2qWaS|88k$fo zZ5>@0!qg0DZefXXbaHlab#q4t1O^4)4+#y6j){E|7oUJhO-s+n%zB=UD=IF*mzI?i zD(V{=o0?y?w7&Y--qG3B-P7AQHa;;qH9a#s_ho5$Wp!d!GGIFXvxJXFvUtVO4

)#D6gxTP}%$4;FgTM!laHZtouOC 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 - DeepGuard Banner -

-

License Stars + Downloads + Last Commit Status Release + Repo Size +

+ +

+ DeepGuard Banner

@@ -43,6 +46,8 @@ - [📈 Model Evaluation](#-model-evaluation) - Benchmarking results - [💻 Model Usage](#-model-usage) - How to integrate DeepGuard models into your own Python code or via timm - [🔮 Predict Image & Video](#-predict-image--video) - Simple Inference examples for detecting deepfakes in image and video +- [🎨 DeepFake AI Explainability](#-deepfake-ai-explainability) - Visualizing model focus using Grad-CAM and attention maps +- [📊 Metrics and Evaluation for DeepFake XAI](#-metrics-and-evaluation-for-deepfake-xai) - Quantitative assessment of explanation reliability - [📬 Authors](#-authors) - [📝 Reference](#-reference) - [⚖️ License](#-license) @@ -303,6 +308,41 @@ print(f"Deepfake Probability: {result:.4f}") ``` +## 🎨 DeepFake AI Explainability + +Deepfake detection requires high reliability and interpretability. DeepGuard integrates a robust Explainable AI (XAI) Toolkit to visualize the decision-making process of our hybrid models + +⭐ Validated on hybrid CNN-ViT architectures, specifically `MS-EffViT` and `MS-EffGCViT`. +⭐ Dual-Branch Analysis: `Low-Level Branch`(focus on local forgery region), `High-Level Branch`(focus on global forgery region) + +### Deepfake Detection: XAI Methods by Multi-Scale Branch + +| Branch | Method | 🎯 What it does | +| :--- | :--- | :--- | +| ![](https://img.shields.io/badge/Low_level-blue?style=flat-square) | **HiResCAM** | Like GradCAM but element-wise multiply the activations with the gradients; provably guaranteed faithfulness for certain models | +| ![](https://img.shields.io/badge/Low_level-blue?style=flat-square) | **GradCAMElementWise** | Like GradCAM but element-wise multiply the activations with the gradients then apply a ReLU operation before summing | +| ![](https://img.shields.io/badge/Low_level-blue?style=flat-square) | **LayerCAM** | Spatially weight the activations by positive gradients. Works better especially in lower layers | +| --- | --- | --- | --- | +| ![](https://img.shields.io/badge/High_level-red?style=flat-square) | **EigenGradCAM** | Like EigenCAM but with class discrimination: First principle component of Activations*Grad. Looks like GradCAM, but cleaner | +| ![](https://img.shields.io/badge/High_level-red?style=flat-square) | **GradCAM++** | Like GradCAM but uses second order gradients | +| ![](https://img.shields.io/badge/High_level-red?style=flat-square) | **XGradCAM** | Like GradCAM but scale the gradients by the normalized activations | + +### Multi Scale Efficient Vision Transformer + +| Model | Branch-Level | Image | HiresCam | GradCamElementwise | LayerCam | +| :--- | :---: | :---: | :---: | :---: | :---: | +| **⚡ ms-eff-vit-b0** | ![](https://img.shields.io/badge/Low_level_Branch-blue?style=flat-square) | | | | | +| **⚡ ms-eff-vit-b5** | ![](https://img.shields.io/badge/Low_level_Branch-blue?style=flat-square) | | | | | + +| Model | Branch-Level | Image | EigenGradCam | GradCamPlusPlus | XGradCam | +| :--- | :---: | :---: | :---: | :---: | :---: | +| **⚡ ms-eff-vit-b0** | ![](https://img.shields.io/badge/High_level_Branch-red?style=flat-square) | | | | | +| **🔥 ms-eff-vit-b5** | ![](https://img.shields.io/badge/High_level_Branch-red?style=flat-square) | | | | | + + + +## 📊 Metrics and Evaluation for DeepFake XAI + ## 📬 Authors @@ -323,6 +363,7 @@ _**This project was developed as a Senior Graduation Project by the Department o 6. [`deepfake-detection-project-v4`](https://github.com/ameencaslam/deepfake-detection-project-v4) - _Multiple Deep Learning Models by Ameen Caslam_ 7. [`Awesome-Deepfake-Detection`](https://github.com/Daisy-Zhang/Awesome-Deepfakes-Detection ) - _A curated list of tools, papers and code by Daisy Zhang_ +8. [`Pytorch-Grad-Cam`](https://github.com/jacobgil/pytorch-grad-cam) - _Advanced Visual Explanations for PyTorch Models_ ## ⚖️ License From 4942477c8a4cce3824dadce7e94617c621104ef6 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Sat, 16 May 2026 18:54:07 +0900 Subject: [PATCH 36/48] =?UTF-8?q?=F0=9F=A7=B9=20Slim=20cache=20key=20to=20?= =?UTF-8?q?(model,=20dataset,=20type,=20branch)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/explain_svc.py | 17 +++--- explainability/explainer/base_explainer.py | 2 - explainability/explainer/cam_explainer.py | 61 +++++++++++----------- 3 files changed, 37 insertions(+), 43 deletions(-) diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py index 6bd0e95..07118a5 100644 --- a/App/services/explain_svc.py +++ b/App/services/explain_svc.py @@ -26,29 +26,26 @@ _explainer_cache: dict = {} def _get_or_create_explainer(model_name: str, dataset: str, explain_req: ExplainRequest): - cache_key = (model_name, dataset, - explain_req.explainer_type, explain_req.branch_level, - explain_req.category, explain_req.aug_smooth, explain_req.eigen_smooth) + cache_key = (model_name, dataset, explain_req.explainer_type, explain_req.branch_level) if cache_key not in _explainer_cache: _explainer_cache[cache_key] = EXPLAINER_REGISTRY[explain_req.explainer_type]( model_name = model_name, dataset = dataset, branch_level = explain_req.branch_level, - category = explain_req.category, - aug_smooth = explain_req.aug_smooth, - eigen_smooth = explain_req.eigen_smooth, ) return _explainer_cache[cache_key] def _run_visualization(explainer, image_path: str, explain_req: ExplainRequest) -> bytes: if explain_req.display_type == "heatmap": - img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio, threshold=explain_req.threshold) + img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio, threshold=explain_req.threshold, + category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) elif explain_req.display_type == "contour": - img = explainer.display_contour_on_image(image_path, threshold=explain_req.threshold) + img = explainer.display_contour_on_image(image_path, threshold=explain_req.threshold, + category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) else: - img = explainer.display_bbox_on_image(image_path, threshold=explain_req.threshold) - + img = explainer.display_bbox_on_image(image_path, threshold=explain_req.threshold, + category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) _, buf = cv2.imencode(".png", img_bgr) return buf.tobytes() diff --git a/explainability/explainer/base_explainer.py b/explainability/explainer/base_explainer.py index 6054772..2897a1e 100644 --- a/explainability/explainer/base_explainer.py +++ b/explainability/explainer/base_explainer.py @@ -18,7 +18,6 @@ def __init__( dataset: str, # celeb_df_v2 | ff++ | both margin_ratio: float = 0.2, conf_thres: float = 0.5, - category: int = 1, # 0: real | 1: fake branch_level: str = "both", # low | high | both l_stage_idx: int = -1, # gcvit low branch stage index (0~3) block_idx: int = -1 # vit, gcvit block index @@ -28,7 +27,6 @@ def __init__( self.device = "cuda:0" if torch.cuda.is_available() else 'cpu' self.margin_ratio = margin_ratio self.conf_thres = conf_thres - self.category = category self.model_name = model_name self.dataset = dataset self.branch_level = branch_level diff --git a/explainability/explainer/cam_explainer.py b/explainability/explainer/cam_explainer.py index 36ea75b..27e6f58 100644 --- a/explainability/explainer/cam_explainer.py +++ b/explainability/explainer/cam_explainer.py @@ -33,35 +33,36 @@ def _draw_label(img: np.ndarray, text: str, x: int, y: int, prob_ratio: float = class CAMExplainer(BaseExplainer, ABC): - def __init__(self, - aug_smooth: bool = False, - eigen_smooth: bool = False, - **kwargs): + def __init__(self, **kwargs): super().__init__(**kwargs) - self.aug_smooth = aug_smooth - self.eigen_smooth = eigen_smooth self.cam = self._build_cam() @abstractmethod def _build_cam(self): ... - def _build_grayscale_cam(self, img_path: str): + def _build_grayscale_cam(self, img_path: str, + category: int = 1, + aug_smooth: bool = False, + eigen_smooth: bool = False): face, tensor = self._get_transform(img_path) tensor = tensor.to(self.device) - targets = [BinaryClassifierOutputTarget(self.category)] + targets = [BinaryClassifierOutputTarget(category)] grayscale_cam = self.cam( input_tensor=tensor, targets=targets, - aug_smooth=self.aug_smooth, - eigen_smooth=self.eigen_smooth, + aug_smooth=aug_smooth, + eigen_smooth=eigen_smooth, ) return grayscale_cam[0], tensor, face - def _prepare_cam(self, img_path: str): + def _prepare_cam(self, img_path: str, + category: int = 1, + aug_smooth: bool = False, + eigen_smooth: bool = False): """Build grayscale CAM and recover it to original face resolution.""" - grayscale_cam, tensor, face = self._build_grayscale_cam(img_path) + grayscale_cam, tensor, face = self._build_grayscale_cam(img_path, category, aug_smooth, eigen_smooth) cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) return cam_recovered, face @@ -75,31 +76,29 @@ def _get_binary_mask(self, cam: np.ndarray, threshold: float | str): ) return binary_mask - def display_heatmap_on_image(self, - img_path: str, - colormap: int = cv2.COLORMAP_JET, - image_weight: float = 0.5, - threshold: float | str = "auto" + def display_heatmap_on_image(self, img_path: str, colormap: int = cv2.COLORMAP_JET, + image_weight: float = 0.5, threshold: float | str = "auto", + category: int = 1, + aug_smooth: bool = False, eigen_smooth: bool = False, ): - cam_recovered, face = self._prepare_cam(img_path) + cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) binary_mask = self._get_binary_mask(cam_recovered, threshold) cam_recovered = np.where(binary_mask > 0, cam_recovered, 0.0) - heatmap = show_cam_on_image( + # (H,W,3), range(0~255), np.uint8 + return show_cam_on_image( np.float32(face) / 255.0, cam_recovered, use_rgb=True, colormap=colormap, image_weight=image_weight, - ) - return heatmap # (H,W,3), range(0~255), np.uint8 + ) - def display_contour_on_image(self, - img_path: str, - threshold: float | str = 'auto', - thickness: int = 2): - cam_recovered, face = self._prepare_cam(img_path) + def display_contour_on_image(self, img_path: str, threshold: float | str = 'auto', thickness: int = 2, + category: int = 1, + aug_smooth: bool = False, eigen_smooth: bool = False): + cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) binary_mask = self._get_binary_mask(cam_recovered, threshold) @@ -107,11 +106,10 @@ def display_contour_on_image(self, annotated_img = cv2.drawContours(face.copy(), contours, -1, (255, 0, 0), thickness) return annotated_img - def display_bbox_on_image(self, - img_path: str, - threshold: float | str = 'auto', - thickness: int = 2): - cam_recovered, face = self._prepare_cam(img_path) + def display_bbox_on_image(self, img_path: str, threshold: float | str = 'auto', thickness: int = 2, + category: int = 1, + aug_smooth: bool = False, eigen_smooth: bool = False): + cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) binary_mask = self._get_binary_mask(cam_recovered, threshold) @@ -127,6 +125,7 @@ def display_bbox_on_image(self, box_color = (r, g, 30) # RGB cv2.rectangle(annotated_img, (x, y), (x + w, y + h), box_color, thickness) + _draw_label( annotated_img, text=f"{roi_prob * 100:.1f}%", From 8043fc69e5b2ddafb5cf3ee235e99bef899f5dd5 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Tue, 19 May 2026 09:16:28 +0900 Subject: [PATCH 37/48] =?UTF-8?q?=E2=9C=A8=20Add=20validate=5Fexplainer=5F?= =?UTF-8?q?for=5Feigen=5Fsmooth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/schemas/explain_schema.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py index b730a51..8dd1773 100644 --- a/App/schemas/explain_schema.py +++ b/App/schemas/explain_schema.py @@ -19,6 +19,8 @@ _LOW_ALLOWED = {"hirescam", "gradcamelementwise", "layercam"} _HIGH_ALLOWED = {"eigengradcam", "gradcamplusplus", "xgradcam"} +_EIGEN_ALLOWED = {"gradcamelementwise", "layercam", "xgradcam"} + class ExplainRequest(BaseModel): branch_level: Literal["low","high"] = Field("high", description="브랜치 레벨\nlow: 국소 위조 흔적 포착\nhigh: 전역적 위조 흔적 포착") @@ -32,11 +34,21 @@ class ExplainRequest(BaseModel): @model_validator(mode="after") def validate_explainer_for_branch(self) -> "ExplainRequest": - allowed = {"low": _LOW_ALLOWED, "high": _HIGH_ALLOWED} - if self.explainer_type not in allowed[self.branch_level]: + branch_allowed = {"low": _LOW_ALLOWED, "high": _HIGH_ALLOWED} + if self.explainer_type not in branch_allowed[self.branch_level]: raise ValueError( f"branch_level='{self.branch_level}'에서 허용된 기법: " - f"{sorted(allowed[self.branch_level])} " + f"{sorted(branch_allowed[self.branch_level])} " + f"(입력값: '{self.explainer_type}')" + ) + return self + + @model_validator(mode="after") + def validate_explainer_for_eigen_smooth(self) -> "ExplainRequest": + if self.eigen_smooth and self.explainer_type not in _EIGEN_ALLOWED: + raise ValueError( + f"eigen_smooth가 허용된 기법: " + f"{sorted(_EIGEN_ALLOWED)} " f"(입력값: '{self.explainer_type}')" ) return self \ No newline at end of file From cfee097eda3c398fb2e5cff02544c46e6916b667 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 09:46:22 +0900 Subject: [PATCH 38/48] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Include=20explain=5F?= =?UTF-8?q?svc,=20image=5Fsvc,=20video=5Fsvc=20in=20celery=5Fapp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/celery_app.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/App/celery_app.py b/App/celery_app.py index df5fd33..5503e8b 100644 --- a/App/celery_app.py +++ b/App/celery_app.py @@ -15,7 +15,11 @@ "deepguard", broker=REDIS_URL, # 메세지를 전달할 브로커 backend=REDIS_URL, # 작업 결과를 저장할 백엔드 - include=["services.inference_svc"] # 비동기 추론 작업 파일 + # 비동기 추론 작업 파일 + include=["services.inference_svc", # process_image_task, process_video_task + "services.explain_svc", # process_explain_image_task + "services.image_svc", # cleanup_anonymous_image, cleanup_image_cam + "services.video_svc"] # cleanup_anonymous_video ) # Celery Instance 상세 설정 From 667e93cf56d62a55a439a3a19e748e23f6967808 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 09:52:28 +0900 Subject: [PATCH 39/48] =?UTF-8?q?=E2=9C=A8=20Add=20get=5Fexplain=5Fimage?= =?UTF-8?q?=5Fresult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 74 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/App/routes/explain.py b/App/routes/explain.py index d15a71d..d6083d4 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -1,16 +1,20 @@ +import os import io +from pydantic import Field from fastapi import APIRouter, status, Depends from fastapi.exceptions import HTTPException -from utils.responses import PNGStreamingResponse +from fastapi.responses import JSONResponse from sqlalchemy import Connection from db.database import context_get_conn from schemas.explain_schema import ExplainRequest from services import session_svc, explain_svc, image_svc +from celery_app import celery_app +from celery.result import AsyncResult router = APIRouter(prefix="/explain", tags=["explain"]) -@router.post("/image/{image_id}", status_code=status.HTTP_200_OK, - response_class=PNGStreamingResponse , summary="딥페이크 이미지 위조 흔적 시각화(CAM)") +@router.post("/image/{image_id}", status_code=status.HTTP_202_ACCEPTED, + response_class=JSONResponse, summary="딥페이크 이미지 위조 흔적 시각화 비동기 접수") async def explain_image( image_id: int, explain_req: ExplainRequest, @@ -18,14 +22,72 @@ async def explain_image( session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 ): result = await image_svc.get_image_result(conn, image_id) - + if result.status != "SUCCESS": raise HTTPException( status_code = status.HTTP_400_BAD_REQUEST, detail = "이미지 위조 흔적 분석은 추론이 성공한 이미지만 가능합니다" ) + + image_path = "." + result.image_loc + + if not os.path.exists(image_path): + raise HTTPException( + status_code = status.HTTP_404_NOT_FOUND, + detail = f"요청하신 이미지 파일을 찾을 수 없습니다. 삭제하였는지 다시 확인해주세요." + ) + + if result.model_type == "pro" and explain_req.aug_smooth: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Pro 모델은 aug_smooth 기능을 지원하지 않습니다", + ) + + # Celery Task 호출(Redis 브로커로 작업 전달) + task = explain_svc.process_explain_image_task.delay( + user_email = session_user["email"], + result_dict = result.model_dump(), # 직렬화 (Pydantic 불가) + explain_req_dict = explain_req.model_dump()) # 직렬화 (Pydantic 불가) + return { + "message": "딥페이크 이미지 위조 흔적 시각화 접수 완료. 시각화 분석 시작 ...", + "task_id": task.id, + } + - img_bytes = await explain_svc.explain_image(conn, result, explain_req) - return PNGStreamingResponse(io.BytesIO(img_bytes)) +@router.get("/image/result/{task_id}", status_code=status.HTTP_200_OK, + response_class=JSONResponse, summary="딥페이크 이미지 위조 흔적 시각화 결과 가져오기") +async def get_explain_image_result( + task_id: str, + session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 + ): + + # celery_app에서 해당 task 가져오기 + task = AsyncResult(task_id, app=celery_app) + + if task.state == "PENDING": + return { + "status": "PENDING", + "message": "딥페이크 이미지 위조 흔적 시각화 분석 중 ..." + } + + if task.state == "FAILURE": + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="딥페이크 이미지 위조 흔적 시각화 중 알 수 없는 오류가 발생하였습니다") + + # Celery Task 성공 시 + result = task.result + + if result["status"] == "FAILED": + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=result["message"]) + + return { + "status": result["status"], + "message": result["message"], + "cam_loc": result["cam_loc"], + } + \ No newline at end of file From 7efe4e134fca669ed5e4f07ff695e1c2483805d3 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 10:02:08 +0900 Subject: [PATCH 40/48] =?UTF-8?q?=E2=9C=A8=20Add=20cleanup=5Fimage=5Fcam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/image_svc.py | 74 +++++++++++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 19 deletions(-) diff --git a/App/services/image_svc.py b/App/services/image_svc.py index e094f6f..5dab7f3 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -1,6 +1,8 @@ import os import asyncio import time +import cv2 +import numpy as np import aiofiles as aio from dotenv import load_dotenv @@ -14,34 +16,30 @@ load_dotenv() -UPLOAD_DIR = os.getenv("UPLOAD_DIR") +UPLOAD_DIR = os.getenv("UPLOAD_DIR", "./static/uploads") +EXPLAIN_UPLOAD_DIR = os.getenv("EXPLAIN_UPLOAD_DIR", "./static/explain") -# 사용자 업로드 이미지 서버 내 저장 (회원/비회원 공통) +os.makedirs(EXPLAIN_UPLOAD_DIR, exist_ok=True) +os.makedirs(UPLOAD_DIR, exist_ok=True) + +# 사용자 업로드 이미지 파일 서버 내 저장 (회원/비회원 공통) async def upload_image(user_email: str | None, imagefile: UploadFile) -> str: try: - # 1. 사용자별 하위 디렉토리 결정 + # 사용자별 하위 디렉토리 결정 sub_dir = user_email if user_email else "anonymous" user_dir = os.path.join(UPLOAD_DIR, sub_dir) - # 3. 디렉토리 존재 확인 및 생성 - if not os.path.exists(user_dir): - try: - os.makedirs(user_dir, exist_ok=True) - except OSError as e: - print(f"[Storage Error] 디렉토리 생성 실패: {e}") - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail="서버 내 저장 공간을 준비하지 못했습니다." - ) - - # 4. 파일명 중복 방지 (파일명_타임스탬프.확장자) + # 디렉토리 존재 확인 및 생성 + os.makedirs(user_dir, exist_ok=True) + + # 파일명 중복 방지 (파일명_타임스탬프.확장자) filename_only, ext = os.path.splitext(imagefile.filename) upload_filename = f"{filename_only}_{int(time.time())}{ext}" upload_image_loc = os.path.join(user_dir, upload_filename) - # 5. 비동기 이미지 저장 (Chunk 단위 읽기) + # 비동기 이미지 저장 (Chunk 단위 읽기) try: async with aio.open(upload_image_loc, "wb") as outfile: while content := await imagefile.read(1024 * 1024): @@ -65,6 +63,25 @@ async def upload_image(user_email: str | None, imagefile: UploadFile) -> str: status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="이미지 업로드 과정에서 예상치 못한 오류가 발생했습니다.") +# 딥페이크 이미지 위조 흔적 시각화 파일 서버 내 저장 (회원 전용) +async def upload_image_cam(user_email: str, image_id: int, image: np.ndarray) -> str: + user_dir = os.path.join(EXPLAIN_UPLOAD_DIR, user_email) + os.makedirs(user_dir, exist_ok=True) + + cam_filename = f"{image_id}_{int(time.time())}.png" + cam_loc = os.path.join(user_dir, cam_filename) + + image_bgr = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) + _, buf = cv2.imencode(".png", image_bgr) + + try: + async with aio.open(cam_loc, "wb") as outfile: + await outfile.write(buf.tobytes()) + except Exception as e: + raise e + + return cam_loc[1:].replace("\\", "/") + # 사용자 업로드 이미지 서버 내 삭제 async def delete_image(image_loc: str): try: @@ -76,8 +93,7 @@ async def delete_image(image_loc: str): else: print(f"File not found: {file_path}") - except Exception as e: - print(f"{e}") + except Exception: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="알수없는 이유로 문제가 발생하였습니다." @@ -185,7 +201,27 @@ async def _delete(): finally: loop.close() - +@celery_app.task(name="cleanup_image_cam") +def cleanup_image_cam(cam_loc: str): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + async def _delete(): + is_deleted = True + try: + await delete_image(cam_loc) + except Exception as e: + is_deleted = False + print(f"[Cleanup] 이미지 시각화 파일 삭제 실패 - cam_loc: {cam_loc}, error: {e}") + + if is_deleted: + print(f"[Cleanup] 이미지 시각화 파일 삭제 프로세스 완료 - cam_loc: {cam_loc}") + + loop.run_until_complete(_delete()) + + finally: + loop.close() + # 이미지 DB 레코드 및 물리 파일 완전 삭제 async def delete_image_db(conn: Connection, image_id: int): try: From 8b89a071a598e619feeb778b2c1f8f414fb91028 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 10:03:03 +0900 Subject: [PATCH 41/48] =?UTF-8?q?=E2=9C=A8=20Add=20process=5Fexplain=5Fima?= =?UTF-8?q?ge=5Ftask?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/explain_svc.py | 127 ++++++++++++++++++++++++------------ 1 file changed, 86 insertions(+), 41 deletions(-) diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py index 07118a5..2367bf1 100644 --- a/App/services/explain_svc.py +++ b/App/services/explain_svc.py @@ -1,5 +1,6 @@ import cv2 import asyncio +import numpy as np from functools import partial from fastapi import status from fastapi.exceptions import HTTPException @@ -8,63 +9,107 @@ from schemas.image_schema import ImageData_indi from services import image_svc, inference_svc from explainability import ( + CAMExplainer, HiResCAMExplainer, GradCAMElementWiseExplainer, LayerCAMExplainer, EigenGradCAMExplainer, GradCAMPlusPlusExplainer, XGradCAMExplainer, ) +from celery_app import celery_app +# --- CAM Explainer Registry --- +# LOW branch: 국소 위조 흔적 포착 +# HIGH branch: 전역 위조 흔적 포착 EXPLAINER_REGISTRY = { - # LOW branch: 국소 위조 흔적 포착 - "hirescam": HiResCAMExplainer, - "gradcamelementwise": GradCAMElementWiseExplainer, - "layercam": LayerCAMExplainer, - # HIGH branch: 전역 위조 흔적 포착 - "eigengradcam": EigenGradCAMExplainer, - "gradcamplusplus": GradCAMPlusPlusExplainer, - "xgradcam": XGradCAMExplainer, + "hirescam": HiResCAMExplainer, #LOW branch + "gradcamelementwise": GradCAMElementWiseExplainer, #LOW branch + "layercam": LayerCAMExplainer, #LOW branch + "eigengradcam": EigenGradCAMExplainer, # HIGH branch + "gradcamplusplus": GradCAMPlusPlusExplainer, # HIGH branch + "xgradcam": XGradCAMExplainer, # HIGH branch } _explainer_cache: dict = {} -def _get_or_create_explainer(model_name: str, dataset: str, explain_req: ExplainRequest): - cache_key = (model_name, dataset, explain_req.explainer_type, explain_req.branch_level) +# 캐시된 CAMExplainer 객체 반환하거나 새로 생성 +def _get_or_create_explainer(model_name: str, dataset: str, explain_req_dict: dict): + cache_key = (model_name, dataset, explain_req_dict["explainer_type"], explain_req_dict["branch_level"]) if cache_key not in _explainer_cache: - _explainer_cache[cache_key] = EXPLAINER_REGISTRY[explain_req.explainer_type]( + _explainer_cache[cache_key] = EXPLAINER_REGISTRY[explain_req_dict["explainer_type"]]( model_name = model_name, dataset = dataset, - branch_level = explain_req.branch_level, + branch_level = explain_req_dict["branch_level"], ) return _explainer_cache[cache_key] -def _run_visualization(explainer, image_path: str, explain_req: ExplainRequest) -> bytes: - if explain_req.display_type == "heatmap": - img = explainer.display_heatmap_on_image(image_path, image_weight=explain_req.overlay_ratio, threshold=explain_req.threshold, - category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) - elif explain_req.display_type == "contour": - img = explainer.display_contour_on_image(image_path, threshold=explain_req.threshold, - category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) +# 시각화 이미지 생성 (heatmap, contour, bbox 선택) +def _run_visualization(explainer: CAMExplainer, image_path: str, explain_req_dict: dict) -> np.ndarray: + if explain_req_dict["display_type"] == "heatmap": + return explainer.display_heatmap_on_image(image_path, image_weight=explain_req_dict["overlay_ratio"], threshold=explain_req_dict["threshold"], + category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) + elif explain_req_dict["display_type"] == "contour": + return explainer.display_contour_on_image(image_path, threshold=explain_req_dict["threshold"], + category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) else: - img = explainer.display_bbox_on_image(image_path, threshold=explain_req.threshold, - category=explain_req.category, aug_smooth=explain_req.aug_smooth, eigen_smooth=explain_req.eigen_smooth) - img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - _, buf = cv2.imencode(".png", img_bgr) - return buf.tobytes() + return explainer.display_bbox_on_image(image_path, threshold=explain_req_dict["threshold"], + category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) -async def explain_image(conn: Connection, result: ImageData_indi, explain_req: ExplainRequest): - - model_name, dataset = inference_svc.MODEL_CONFIG[result.version_type][result.model_type][result.domain_type] - - explainer = _get_or_create_explainer(model_name, dataset, explain_req) - image_path = "." + result.image_loc - +# 딥페이크 이미지 위조 흔적 시각화 처리 +@celery_app.task(name="process_explain_image_task") +def process_explain_image_task(user_email: str, + result_dict: dict, + explain_req_dict: dict): + async def run_explain(): + cam_loc = None + try: + # 추론에 사용된 모델 및 데이터셋 설정 로드 + version_type = result_dict["version_type"] + model_type = result_dict["model_type"] + domain_type = result_dict["domain_type"] + + model_name, dataset = inference_svc.MODEL_CONFIG[version_type][model_type][domain_type] + + explainer = _get_or_create_explainer(model_name, dataset, explain_req_dict) + image_path = "." + result_dict["image_loc"] + + # 시각화 이미지 생성 시작 + try: + image = _run_visualization(explainer, image_path, explain_req_dict) + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="딥페이크 이미지 위조 흔적을 생성하는 중 오류가 발생하였습니다" + ) + + # 생성된 시각화 이미지 파일 저장 + try: + cam_loc = await image_svc.upload_image_cam(user_email, result_dict["image_id"], image) + except Exception: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="딥페이크 이미지 위조 흔적 파일을 저장하는 중 오류가 발생했습니다." + ) + + return {"status": "SUCCESS", + "message": "딥페이크 이미지 위조 흔적 시각화가 성공적으로 이루어졌습니다", + "cam_loc": cam_loc} + + except HTTPException as e: + print(e.detail) + return {"status": "FAILED", "message": str(e.detail)} + + except Exception as e: + print(str(e)) + return {"status": "FAILED", "message": str(e)} + + finally: + # 임시 저장된 시각화 이미지 파일 삭제 + if cam_loc: + image_svc.cleanup_image_cam.apply_async(args=[cam_loc], countdown=60) + + # 동기식 Celery 워커 내 비동기 이벤트 루프 구동 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) try: - loop = asyncio.get_event_loop() - return await loop.run_in_executor( - None, - partial(_run_visualization, explainer, image_path, explain_req) - ) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail="알수없는 이유로 문제가 발생하였습니다." - ) + return loop.run_until_complete(run_explain()) + finally: + loop.close() From 1aa3e14b92651db641d790457a96da42f417af50 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 10:22:14 +0900 Subject: [PATCH 42/48] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Add=20Celery=20Task?= =?UTF-8?q?=20Status(STARTED,=20RETRY)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/App/routes/explain.py b/App/routes/explain.py index d6083d4..1ad8552 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -20,7 +20,8 @@ async def explain_image( explain_req: ExplainRequest, conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 -): +): + # 딥페이크 이미지 추론 결과 가져오기 result = await image_svc.get_image_result(conn, image_id) if result.status != "SUCCESS": @@ -43,11 +44,11 @@ async def explain_image( detail="Pro 모델은 aug_smooth 기능을 지원하지 않습니다", ) - # Celery Task 호출(Redis 브로커로 작업 전달) + # Celery Task 호출(Redis Broker 활용) task = explain_svc.process_explain_image_task.delay( user_email = session_user["email"], - result_dict = result.model_dump(), # 직렬화 (Pydantic 불가) - explain_req_dict = explain_req.model_dump()) # 직렬화 (Pydantic 불가) + result_dict = result.model_dump(mode='json'), + explain_req_dict = explain_req.model_dump()) return { "message": "딥페이크 이미지 위조 흔적 시각화 접수 완료. 시각화 분석 시작 ...", "task_id": task.id, @@ -61,10 +62,10 @@ async def get_explain_image_result( session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 ): - # celery_app에서 해당 task 가져오기 + # Redis Broker에서 Task ID에 해당하는 비동기 작업 상태 가져오기 task = AsyncResult(task_id, app=celery_app) - if task.state == "PENDING": + if task.state in ("PENDING", "STARTED", "RETRY"): return { "status": "PENDING", "message": "딥페이크 이미지 위조 흔적 시각화 분석 중 ..." @@ -75,7 +76,7 @@ async def get_explain_image_result( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="딥페이크 이미지 위조 흔적 시각화 중 알 수 없는 오류가 발생하였습니다") - # Celery Task 성공 시 + # Celery Task 결과 가져오기 result = task.result if result["status"] == "FAILED": From bf2a7b002dfa9409185f9a5185a25e55d5afd8ff Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 10:24:34 +0900 Subject: [PATCH 43/48] =?UTF-8?q?=E2=9A=B0=EF=B8=8F=20Remove=20is=5Fdelete?= =?UTF-8?q?d=20flag=20in=20cleanup=5Fimage=5Fcam?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/services/image_svc.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/App/services/image_svc.py b/App/services/image_svc.py index 5dab7f3..f83194e 100644 --- a/App/services/image_svc.py +++ b/App/services/image_svc.py @@ -1,15 +1,14 @@ import os -import asyncio import time import cv2 import numpy as np import aiofiles as aio +import asyncio from dotenv import load_dotenv - from fastapi import UploadFile, status from fastapi.exceptions import HTTPException from sqlalchemy import text, Connection -from sqlalchemy.exc import SQLAlchemyError, DBAPIError +from sqlalchemy.exc import SQLAlchemyError from schemas.image_schema import UserHistory, UserHistory_indi, ImageData_indi from db.database import celery_db_conn from celery_app import celery_app @@ -207,15 +206,11 @@ def cleanup_image_cam(cam_loc: str): asyncio.set_event_loop(loop) try: async def _delete(): - is_deleted = True try: await delete_image(cam_loc) + print(f"[Cleanup] 이미지 시각화 파일 삭제 완료 - cam_loc: {cam_loc}") except Exception as e: - is_deleted = False print(f"[Cleanup] 이미지 시각화 파일 삭제 실패 - cam_loc: {cam_loc}, error: {e}") - - if is_deleted: - print(f"[Cleanup] 이미지 시각화 파일 삭제 프로세스 완료 - cam_loc: {cam_loc}") loop.run_until_complete(_delete()) @@ -233,9 +228,7 @@ async def delete_image_db(conn: Connection, image_id: int): await conn.commit() - except SQLAlchemyError as e: - print(e) await conn.rollback() raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="요청하신 서비스가 잠시 내부적으로 문제가 발생하였습니다.") From a64a4838267ceb87b09506b2cb3b38933ddd1eae Mon Sep 17 00:00:00 2001 From: seoyunje Date: Wed, 20 May 2026 10:47:52 +0900 Subject: [PATCH 44/48] =?UTF-8?q?=F0=9F=9A=9A=20Rename=20ExplainRequest=20?= =?UTF-8?q?to=20ExplainImageRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/routes/explain.py | 4 ++-- App/schemas/explain_schema.py | 6 +++--- App/services/explain_svc.py | 2 -- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/App/routes/explain.py b/App/routes/explain.py index 1ad8552..674edcb 100644 --- a/App/routes/explain.py +++ b/App/routes/explain.py @@ -6,7 +6,7 @@ from fastapi.responses import JSONResponse from sqlalchemy import Connection from db.database import context_get_conn -from schemas.explain_schema import ExplainRequest +from schemas.explain_schema import ExplainImageRequest from services import session_svc, explain_svc, image_svc from celery_app import celery_app from celery.result import AsyncResult @@ -17,7 +17,7 @@ response_class=JSONResponse, summary="딥페이크 이미지 위조 흔적 시각화 비동기 접수") async def explain_image( image_id: int, - explain_req: ExplainRequest, + explain_req: ExplainImageRequest, conn: Connection = Depends(context_get_conn), session_user = Depends(session_svc.get_session_user_prt), # 로그인 필수 ): diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py index 8dd1773..3ce9729 100644 --- a/App/schemas/explain_schema.py +++ b/App/schemas/explain_schema.py @@ -22,7 +22,7 @@ _EIGEN_ALLOWED = {"gradcamelementwise", "layercam", "xgradcam"} -class ExplainRequest(BaseModel): +class ExplainImageRequest(BaseModel): branch_level: Literal["low","high"] = Field("high", description="브랜치 레벨\nlow: 국소 위조 흔적 포착\nhigh: 전역적 위조 흔적 포착") explainer_type: str = Field("eigengradcam", description = ("선택 가능한 XAI 기법. low: [hirescam, gradcamelementwise, layercam], ""high: [eigengradcam, gradcamplusplus, xgradcam]")) display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") @@ -33,7 +33,7 @@ class ExplainRequest(BaseModel): eigen_smooth: bool = Field(False, description = "PCA 기반 노이즈 제거. 지배적인 패턴만 남김") @model_validator(mode="after") - def validate_explainer_for_branch(self) -> "ExplainRequest": + def validate_explainer_for_branch(self) -> "ExplainImageRequest": branch_allowed = {"low": _LOW_ALLOWED, "high": _HIGH_ALLOWED} if self.explainer_type not in branch_allowed[self.branch_level]: raise ValueError( @@ -44,7 +44,7 @@ def validate_explainer_for_branch(self) -> "ExplainRequest": return self @model_validator(mode="after") - def validate_explainer_for_eigen_smooth(self) -> "ExplainRequest": + def validate_explainer_for_eigen_smooth(self) -> "ExplainImageRequest": if self.eigen_smooth and self.explainer_type not in _EIGEN_ALLOWED: raise ValueError( f"eigen_smooth가 허용된 기법: " diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py index 2367bf1..54953f4 100644 --- a/App/services/explain_svc.py +++ b/App/services/explain_svc.py @@ -5,8 +5,6 @@ from fastapi import status from fastapi.exceptions import HTTPException from sqlalchemy import Connection -from schemas.explain_schema import ExplainRequest -from schemas.image_schema import ImageData_indi from services import image_svc, inference_svc from explainability import ( CAMExplainer, From aabfc70780cf6b5ab3705da89917d3d0ac5546f6 Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 21 May 2026 10:36:04 +0900 Subject: [PATCH 45/48] =?UTF-8?q?=E2=9C=A8=20Add=20=5Fget=5Ftransform=5Ffr?= =?UTF-8?q?om=5Farray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/explainer/base_explainer.py | 101 ++++++++++++++++----- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/explainability/explainer/base_explainer.py b/explainability/explainer/base_explainer.py index 2897a1e..5d46ce4 100644 --- a/explainability/explainer/base_explainer.py +++ b/explainability/explainer/base_explainer.py @@ -3,26 +3,22 @@ import torch import numpy as np from pathlib import Path -from typing import List, Optional, Tuple +from typing import List, Optional, Tuple, Literal, Callable from preprocess.face_detector import FaceDetector2 from deepguard.data import get_test_transforms from explainability.utils.reshape_transforms import reshape_transform_vit, reshape_transform_gcvit -DATASETS = {"celeb_df_v2", "ff++", "kodf"} -MODELS = {"ms_eff_vit_b0", "ms_eff_vit_b5", "ms_eff_gcvit_b0", "ms_eff_gcvit_b5"} - class BaseExplainer: def __init__( self, - model_name: str, # ms_eff_vit_b0 | ms_eff_vit_b5 | ms_eff_gcvit_b0 | ms_eff_gcvit_b5 - dataset: str, # celeb_df_v2 | ff++ | both + model_name: Literal["ms_eff_vit_b0", "ms_eff_vit_b5", "ms_eff_gcvit_b0", "ms_eff_gcvit_b5"], + dataset: Literal["celeb_df_v2", "ff++", "kodf"], margin_ratio: float = 0.2, conf_thres: float = 0.5, - branch_level: str = "both", # low | high | both + branch_level: Literal["low", "high"] = "high", l_stage_idx: int = -1, # gcvit low branch stage index (0~3) - block_idx: int = -1 # vit, gcvit block index + block_idx: int = -1, # vit, gcvit block index ): - self._validate_inputs(model_name, dataset, branch_level) self.device = "cuda:0" if torch.cuda.is_available() else 'cpu' self.margin_ratio = margin_ratio @@ -48,19 +44,24 @@ def __init__( # Explainability setup self.reshape_fn = self._get_reshape_fn() self.target_layers = self._resolve_target_layers() - - def _validate_inputs(self, model_name: str, dataset: str, branch_level: str): - if model_name not in MODELS: - raise ValueError(f"Unsupported model: '{model_name}'. Must be one of {MODELS}") - if dataset not in DATASETS: - raise ValueError(f"Invalid dataset: '{dataset}'. Choose from {DATASETS}") - if branch_level not in ("low", "high", "both"): - raise ValueError(f"Invalid branch_level: '{branch_level}'. Expected 'low', 'high', or 'both'.") - - def _get_reshape_fn(self): + + def _get_reshape_fn(self) -> Callable[[torch.Tensor], torch.Tensor]: + """모델 종류에 맞는 Feature map reshape 함수를 반환한다. + + Returns: + Callable: 3D/4D 텐서를 GradCAM 연산에 적합한 형태로 변환하는 함수. + """ return reshape_transform_vit if self.transformer_type == "vit" else reshape_transform_gcvit - def _resolve_target_layers(self) -> list: + def _resolve_target_layers(self) -> list[torch.nn.Module]: + """시각화(XAI)의 대상이 될 Target Layer들을 선택한다. + + Returns: + list[torch.nn.Module]: 분석 대상 정규화(Normalization) 레이어 리스트. + + Raises: + ValueError: l_stage_idx나 block_idx가 모델 범위를 벗어날 경우 발생. + """ layers = [] if self.transformer_type == "gcvit": @@ -95,16 +96,46 @@ def _resolve_target_layers(self) -> list: @staticmethod def _resolve_block_idx(block_idx: int, n_blocks: int, name: str) -> int: + """음수 인덱싱을 지원하며, 해당 블록의 유효한 인덱스를 계산 및 검증한다. + + Args: + block_idx (int): 접근하려는 블록 인덱스 (음수 가능). + n_blocks (int): 해당 스테이지/모델의 총 블록 개수. + name (str): 에러 메시지 출력용 모듈 이름. + + Returns: + int: 정제된 양수 형태의 블록 인덱스. + + Raises: + ValueError: 인덱스가 유효 범위를 벗어난 경우 발생. + """ idx = block_idx if block_idx >= 0 else n_blocks + block_idx if not (0 <= idx < n_blocks): raise ValueError(f"block_idx={block_idx} 범위 초과 ({name} block 수: {n_blocks})") return idx def _get_face_bbox(self, img: np.ndarray) -> List[float]: + """얼굴 탐지기(YOLO)를 통해 이미지에서 얼굴 바운딩 박스를 좌표를 추출한다. + + Args: + img (np.ndarray): HWC 형태의 RGB 이미지 데이터. + + Returns: + list[float]: [xmin, ymin, xmax, ymax, confidence] 형태의 좌표 리스트. + """ result = self.face_detector.detect_batch([img], [1.0]) return result[0] def _crop_face(self, img: np.ndarray, bbox: List[float]) -> np.ndarray: + """마진 비율을 고려하여 바운딩 박스 기준으로 얼굴 영역을 크롭한다. + + Args: + img (np.ndarray): 전체 원본 이미지. + bbox (list[float]): 탐지된 [xmin, ymin, xmax, ymax] 얼굴 좌표. + + Returns: + np.ndarray: 크롭된 얼굴 이미지 영역. + """ xmin, ymin, xmax, ymax = bbox h, w = img.shape[:2] pw = int((xmax - xmin) * self.margin_ratio) @@ -115,6 +146,14 @@ def _crop_face(self, img: np.ndarray, bbox: List[float]) -> np.ndarray: ] def _preprocess_img(self, img_path: str) -> np.ndarray: + """이미지 경로를 읽어 얼굴을 탐지하고 크롭하는 전과정을 수행한다. + + Args: + img_path (str): 입력 이미지 파일 경로. + + Returns: + np.ndarray: 전처리가 완료된 크롭된 얼굴 이미지 (RGB). + """ img = cv2.imread(img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) detect_result = self._get_face_bbox(img) @@ -122,10 +161,30 @@ def _preprocess_img(self, img_path: str) -> np.ndarray: cropped = self._crop_face(img, bbox) return cropped - def _get_transform(self, img_path: str): + def _get_transform(self, img_path: str) -> tuple[np.ndarray, torch.Tensor]: + """이미지 경로를 받아 크롭 이미지와 모델 입력용 텐서를 동시에 생성한다. + + Args: + img_path (str): 입력 이미지 파일 경로. + + Returns: + tuple[np.ndarray, torch.Tensor]: (크롭된 얼굴 이미지, (1, 3, H, W) 형태의 배치 텐서). + """ face = self._preprocess_img(img_path) tensor = self.transforms(image=face)['image'].unsqueeze(0) # (1,3,H,W) return face, tensor + + def _get_transform_from_array(self, face: np.ndarray) -> tuple[np.ndarray, torch.Tensor]: + """이미 크롭된 얼굴 배열을 받아 모델 입력용 텐서로 변환한다. + + Args: + face (np.ndarray): 이미 잘려진 얼굴 이미지 배열 (RGB). + + Returns: + tuple[np.ndarray, torch.Tensor]: (원본 얼굴 이미지, (1, 3, H, W) 형태의 배치 텐서). + """ + tensor = self.transforms(image=face)['image'].unsqueeze(0) # (1,3,H,W) + return face, tensor def __repr__(self): return (f"{self.__class__.__name__}(" From 3474cb82ed4054f2001360dd88b3803ef6b3d16b Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 21 May 2026 11:12:52 +0900 Subject: [PATCH 46/48] =?UTF-8?q?=F0=9F=94=A5=20Remove=20contour=20display?= =?UTF-8?q?=20type=20from=20image=20explainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/schemas/explain_schema.py | 2 +- App/services/explain_svc.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/App/schemas/explain_schema.py b/App/schemas/explain_schema.py index 3ce9729..37a51d8 100644 --- a/App/schemas/explain_schema.py +++ b/App/schemas/explain_schema.py @@ -25,7 +25,7 @@ class ExplainImageRequest(BaseModel): branch_level: Literal["low","high"] = Field("high", description="브랜치 레벨\nlow: 국소 위조 흔적 포착\nhigh: 전역적 위조 흔적 포착") explainer_type: str = Field("eigengradcam", description = ("선택 가능한 XAI 기법. low: [hirescam, gradcamelementwise, layercam], ""high: [eigengradcam, gradcamplusplus, xgradcam]")) - display_type: Literal["heatmap", "contour", "bbox"] = Field("heatmap", description="시각화 형태. heatmap: 전체 분포, contour: 외곽선, bbox: 위조 의심 영역 사각형") + display_type: Literal["heatmap", "bbox"] = Field("heatmap", description="시각화 형태. heatmap: 전체 분포, bbox: 위조 의심 영역 사각형") category: Literal[0, 1] = Field(1, description="판단 클래스 인덱스 (0: Real / 1: Fake)") overlay_ratio: float = Field(0.5, ge=0.0, le=1.0, description = "Heatmap 투명도 (0: 히트맵만 강조, 1: 원본 이미지 위주)") threshold: float = Field(0.5, ge=0.5, le=1.0, description="contour/bbox 이진화 임계값 (0.0~1.0)") diff --git a/App/services/explain_svc.py b/App/services/explain_svc.py index 54953f4..83acc09 100644 --- a/App/services/explain_svc.py +++ b/App/services/explain_svc.py @@ -44,9 +44,6 @@ def _run_visualization(explainer: CAMExplainer, image_path: str, explain_req_dic if explain_req_dict["display_type"] == "heatmap": return explainer.display_heatmap_on_image(image_path, image_weight=explain_req_dict["overlay_ratio"], threshold=explain_req_dict["threshold"], category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) - elif explain_req_dict["display_type"] == "contour": - return explainer.display_contour_on_image(image_path, threshold=explain_req_dict["threshold"], - category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) else: return explainer.display_bbox_on_image(image_path, threshold=explain_req_dict["threshold"], category=explain_req_dict["category"], aug_smooth=explain_req_dict["aug_smooth"], eigen_smooth=explain_req_dict["eigen_smooth"]) From 4b59f54ecaafec984845b542449349e3ceafa1ed Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 21 May 2026 11:19:11 +0900 Subject: [PATCH 47/48] =?UTF-8?q?=F0=9F=93=9D=20Update=20=5F=5Frepr=5F=5F?= =?UTF-8?q?=20with=20removing=20unnecessary=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/explainer/base_explainer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/explainability/explainer/base_explainer.py b/explainability/explainer/base_explainer.py index 5d46ce4..dc4fc62 100644 --- a/explainability/explainer/base_explainer.py +++ b/explainability/explainer/base_explainer.py @@ -191,4 +191,4 @@ def __repr__(self): f"model={self.model_name}, dataset={self.dataset}, " f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, block_idx={self.block_idx}," - f"category={self.category},device={self.device})") \ No newline at end of file + f"device={self.device})") \ No newline at end of file From d8fafd99aafc9f28cd5cbafb877adb48334a1b5b Mon Sep 17 00:00:00 2001 From: seoyunje Date: Thu, 21 May 2026 11:29:11 +0900 Subject: [PATCH 48/48] =?UTF-8?q?=E2=9C=A8=20Add=20display=5Fheatmap=5Ffro?= =?UTF-8?q?m=5Farray,=20display=5Fbbox=5Ffrom=5Farray?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- explainability/explainer/cam_explainer.py | 269 ++++++++++++++++++---- 1 file changed, 229 insertions(+), 40 deletions(-) diff --git a/explainability/explainer/cam_explainer.py b/explainability/explainer/cam_explainer.py index 27e6f58..f630055 100644 --- a/explainability/explainer/cam_explainer.py +++ b/explainability/explainer/cam_explainer.py @@ -1,3 +1,4 @@ +from typing import Union, Literal import torch import cv2 import numpy as np @@ -38,13 +39,33 @@ def __init__(self, **kwargs): self.cam = self._build_cam() @abstractmethod - def _build_cam(self): - ... + def _build_cam(self) -> any: + """하위 클래스에서 각 알고리즘(GradCAM, HiResCAM 등)에 맞는 CAM 객체를 생성한다. + + Returns: + Any: pytorch-grad-cam 패키지의 CAM 기반 인스턴스. + """ + pass def _build_grayscale_cam(self, img_path: str, category: int = 1, aug_smooth: bool = False, - eigen_smooth: bool = False): + eigen_smooth: bool = False) -> tuple[np.ndarray, torch.Tensor, np.ndarray]: + """이미지 경로를 받아 전처리를 수행하고 raw 흑백(Grayscale) CAM을 생성한다. + + Args: + img_path (str): 입력 이미지 파일 경로. + category (int, optional): 타겟 클래스 인덱스 (위조: 1, 정상: 0). Defaults to 1. + aug_smooth (bool, optional): Test-time augmentation 가동 여부. Defaults to False. + eigen_smooth (bool, optional): PCA 기반 노이즈 제거 가동 여부. Defaults to False. + + Returns: + tuple[np.ndarray, torch.Tensor, np.ndarray]: + - grayscale_cam: (H, W) 형태의 0~1 사이로 정규화된 CAM 맵. + - tensor: (1, 3, H_model, W_model) 형태의 모델 입력용 GPU 텐서. + - face: 크롭된 원본 얼굴 이미지 배열 (RGB). + """ + face, tensor = self._get_transform(img_path) tensor = tensor.to(self.device) targets = [BinaryClassifierOutputTarget(category)] @@ -56,17 +77,92 @@ def _build_grayscale_cam(self, img_path: str, eigen_smooth=eigen_smooth, ) return grayscale_cam[0], tensor, face + + def _build_grayscale_cam_from_array(self, + face: np.ndarray, + category: int = 1, + aug_smooth: bool = False, + eigen_smooth: bool = False) -> tuple[np.ndarray, torch.Tensor, np.ndarray]: + """이미 크롭된 얼굴 이미지 배열로부터 raw 흑백(Grayscale) CAM을 생성한다. + + Args: + face (np.ndarray): 전처리(크롭)가 완료된 얼굴 이미지 배열 (RGB). + category (int, optional): 타겟 클래스 인덱스. Defaults to 1. + aug_smooth (bool, optional): Test-time augmentation 가동 여부. Defaults to False. + eigen_smooth (bool, optional): PCA 기반 노이즈 제거 가동 여부. Defaults to False. + + Returns: + tuple[np.ndarray, torch.Tensor, np.ndarray]: + - grayscale_cam: (H, W) 형태의 0~1 사이로 정규화된 CAM 맵. + - tensor: (1, 3, H_model, W_model) 형태의 모델 입력용 GPU 텐서. + - face: 입력받은 원본 얼굴 이미지 배열. + """ + face, tensor = self._get_transform_from_array(face) + tensor = tensor.to(self.device) + targets = [BinaryClassifierOutputTarget(category)] + + grayscale_cam = self.cam( + input_tensor=tensor, + targets=targets, + aug_smooth=aug_smooth, + eigen_smooth=eigen_smooth, + ) + return grayscale_cam[0], tensor, face def _prepare_cam(self, img_path: str, category: int = 1, aug_smooth: bool = False, - eigen_smooth: bool = False): - """Build grayscale CAM and recover it to original face resolution.""" + eigen_smooth: bool = False) -> tuple[np.ndarray, np.ndarray]: + """흑백 CAM을 생성하고, 모델 입력 시 발생한 패딩 제거 및 원래 얼굴 해상도로 복원한다. + + Args: + img_path (str): 입력 이미지 파일 경로. + category (int, optional): 타겟 클래스 인덱스. Defaults to 1. + aug_smooth (bool, optional): Test-time augmentation 가동 여부. Defaults to False. + eigen_smooth (bool, optional): PCA 기반 노이즈 제거 가동 여부. Defaults to False. + + Returns: + tuple[np.ndarray, np.ndarray]: + - cam_recovered: 원본 얼굴 크기로 복원된 CAM 맵 (H_face, W_face). + - face: 크롭된 원본 얼굴 이미지 배열 (RGB). + """ grayscale_cam, tensor, face = self._build_grayscale_cam(img_path, category, aug_smooth, eigen_smooth) cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) return cam_recovered, face - def _get_binary_mask(self, cam: np.ndarray, threshold: float | str): + def _prepare_cam_from_array(self, face: np.ndarray, + category: int = 1, + aug_smooth: bool = False, + eigen_smooth: bool = False) -> tuple[np.ndarray, np.ndarray]: + """얼굴 배열로부터 흑백 CAM을 생성하고 원래 얼굴 해상도로 복원한다. + + Args: + face (np.ndarray): 전처리(크롭)가 완료된 얼굴 이미지 배열 (RGB). + category (int, optional): 타겟 클래스 인덱스. Defaults to 1. + aug_smooth (bool, optional): Test-time augmentation 가동 여부. Defaults to False. + eigen_smooth (bool, optional): PCA 기반 노이즈 제거 가동 여부. Defaults to False. + + Returns: + tuple[np.ndarray, np.ndarray]: + - cam_recovered: 원본 얼굴 크기로 복원된 CAM 맵 (H_face, W_face). + - face: 입력받은 원본 얼굴 이미지 배열. + """ + grayscale_cam, tensor, face = self._build_grayscale_cam_from_array(face, category, aug_smooth, eigen_smooth) + cam_recovered = remove_padding_and_resize(grayscale_cam, face.shape, tensor.shape) + return cam_recovered, face + + def _get_binary_mask(self, cam: np.ndarray, threshold: Union[float, Literal["auto"]]) -> np.ndarray: + """입력된 CAM 맵을 기반으로 위조 유력 부위를 이진화(Binary Mask)한다. + + Args: + cam (np.ndarray): 0~1 사이 값을 가진 복원된 CAM 맵. + threshold (Union[float, 'auto']): + - float (0.0~1.0): 상위 백분위수 기준으로 임계값 설정 (예: 0.7 이면 상위 30% 영역 선택). + - 'auto': Otsu 알고리즘을 사용해 최적의 임계값을 자동으로 계산. + + Returns: + np.ndarray: 0 또는 255 값을 가지는 이진화 마스크 맵 (np.uint8). + """ if threshold != 'auto': t = np.percentile(cam, threshold * 100) binary_mask = (cam > t).astype(np.uint8) * 255 @@ -76,42 +172,143 @@ def _get_binary_mask(self, cam: np.ndarray, threshold: float | str): ) return binary_mask - def display_heatmap_on_image(self, img_path: str, colormap: int = cv2.COLORMAP_JET, - image_weight: float = 0.5, threshold: float | str = "auto", - category: int = 1, - aug_smooth: bool = False, eigen_smooth: bool = False, - ): - cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) + def display_heatmap_on_image(self, img_path: str, **kwargs) -> np.ndarray: + """이미지 경로를 입력받아 원본 얼굴 위에 위조 흔적 히트맵을 중첩한 이미지를 생성한다. + + Args: + img_path (str): 입력 이미지 파일 경로. + **kwargs: + category (int): 타겟 클래스 인덱스 (기본값: 1). + aug_smooth (bool): Test-time augmentation 가동 여부 (기본값: False). + eigen_smooth (bool): PCA 기반 노이즈 제거 가동 여부 (기본값: False). + threshold (float | str): 이진화 임계값 (기본값: "auto"). + colormap (int): OpenCV 컬러맵 상수 (기본값: cv2.COLORMAP_JET). + image_weight (float): 원본 이미지 불투명도 가중치 (기본값: 0.5). + + Returns: + np.ndarray: 히트맵이 합성된 얼굴 이미지 (RGB, np.uint8). + """ + cam_recovered, face = self._prepare_cam( + img_path, + category = kwargs.get("category", 1), + aug_smooth = kwargs.get("aug_smooth", False), + eigen_smooth = kwargs.get("eigen_smooth", False)) + + binary_mask = self._get_binary_mask(cam_recovered, kwargs.get("threshold", "auto")) + cam_recovered = np.where(binary_mask > 0, cam_recovered, 0.0) + + return show_cam_on_image( + np.float32(face) / 255.0, + cam_recovered, + use_rgb = True, + colormap = kwargs.get("colormap", cv2.COLORMAP_JET), + image_weight = kwargs.get("image_weight", 0.5), + ) + + def display_heatmap_from_array(self, face: np.ndarray, **kwargs) -> np.ndarray: + """얼굴 배열을 직접 입력받아 원본 얼굴 위에 위조 흔적 히트맵을 중첩한 이미지를 생성한다. + + Args: + face (np.ndarray): 전처리(크롭)가 완료된 얼굴 이미지 배열 (RGB). + **kwargs: + category (int): 타겟 클래스 인덱스 (기본값: 1). + aug_smooth (bool): Test-time augmentation 가동 여부 (기본값: False). + eigen_smooth (bool): PCA 기반 노이즈 제거 가동 여부 (기본값: False). + threshold (float | str): 이진화 임계값 (기본값: "auto"). + colormap (int): OpenCV 컬러맵 상수 (기본값: cv2.COLORMAP_JET). + image_weight (float): 원본 이미지 불투명도 가중치 (기본값: 0.5). + + Returns: + np.ndarray: 히트맵이 합성된 얼굴 이미지 (RGB, np.uint8). + """ - binary_mask = self._get_binary_mask(cam_recovered, threshold) + cam_recovered, face = self._prepare_cam_from_array( + face, + category = kwargs.get("category", 1), + aug_smooth = kwargs.get("aug_smooth", False), + eigen_smooth = kwargs.get("eigen_smooth", False)) + + binary_mask = self._get_binary_mask(cam_recovered, kwargs.get("threshold", "auto")) cam_recovered = np.where(binary_mask > 0, cam_recovered, 0.0) - # (H,W,3), range(0~255), np.uint8 return show_cam_on_image( np.float32(face) / 255.0, cam_recovered, - use_rgb=True, - colormap=colormap, - image_weight=image_weight, + use_rgb = True, + colormap = kwargs.get("colormap", cv2.COLORMAP_JET), + image_weight = kwargs.get("image_weight", 0.5), ) - def display_contour_on_image(self, img_path: str, threshold: float | str = 'auto', thickness: int = 2, - category: int = 1, - aug_smooth: bool = False, eigen_smooth: bool = False): - cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) + def display_bbox_on_image(self, img_path: str, **kwargs) -> np.ndarray: + """이미지 경로를 입력받아 위조 의심 부위를 찾아 사각형 바운딩 박스와 확률을 그린다. + + Args: + img_path (str): 입력 이미지 파일 경로. + **kwargs: + category (int): 타겟 클래스 인덱스 (기본값: 1). + aug_smooth (bool): Test-time augmentation 가동 여부 (기본값: False). + eigen_smooth (bool): PCA 기반 노이즈 제거 가동 여부 (기본값: False). + threshold (float | str): 이진화 임계값 (기본값: "auto"). + thickness (int): 사각형 선 굵기 (기본값: 2). + + Returns: + np.ndarray: 바운딩 박스와 어노테이션이 추가된 원본 얼굴 이미지 (RGB). + """ + cam_recovered, face = self._prepare_cam( + img_path, + category = kwargs.get("category", 1), + aug_smooth = kwargs.get("aug_smooth", False), + eigen_smooth = kwargs.get("eigen_smooth", False) + ) + + binary_mask = self._get_binary_mask(cam_recovered, kwargs.get("threshold", "auto")) - binary_mask = self._get_binary_mask(cam_recovered, threshold) - contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - annotated_img = cv2.drawContours(face.copy(), contours, -1, (255, 0, 0), thickness) + + annotated_img = face.copy() + for c in contours: + x, y, w, h = cv2.boundingRect(c) + roi_prob = float(np.max(cam_recovered[y:y + h, x:x + w])) + + r = int(min(255, 2 * roi_prob * 255)) + g = int(min(255, 2 * (1 - roi_prob) * 255)) + box_color = (r, g, 30) # RGB + + cv2.rectangle(annotated_img, (x, y), (x + w, y + h), box_color, kwargs.get("thickness", 1)) + + _draw_label( + annotated_img, + text=f"{roi_prob * 100:.1f}%", + x=x, + y=max(y - 6, 14), + prob_ratio=roi_prob, + ) + return annotated_img - - def display_bbox_on_image(self, img_path: str, threshold: float | str = 'auto', thickness: int = 2, - category: int = 1, - aug_smooth: bool = False, eigen_smooth: bool = False): - cam_recovered, face = self._prepare_cam(img_path, category, aug_smooth, eigen_smooth) + + def display_bbox_from_array(self, face: np.ndarray, **kwargs) -> np.ndarray: + """얼굴 배열을 직접 입력받아 위조 의심 부위를 찾아 사각형 바운딩 박스와 확률을 그린다. + + Args: + face (np.ndarray): 전처리(크롭)가 완료된 얼굴 이미지 배열 (RGB). + **kwargs: + category (int): 타겟 클래스 인덱스 (기본값: 1). + aug_smooth (bool): Test-time augmentation 가동 여부 (기본값: False). + eigen_smooth (bool): PCA 기반 노이즈 제거 가동 여부 (기본값: False). + threshold (float | str): 이진화 임계값 (기본값: "auto"). + thickness (int): 사각형 선 굵기 (기본값: 2). + + Returns: + np.ndarray: 바운딩 박스와 어노테이션이 추가된 원본 얼굴 이미지 (RGB). + """ + cam_recovered, face = self._prepare_cam_from_array( + face, + category = kwargs.get("category", 1), + aug_smooth = kwargs.get("aug_smooth", False), + eigen_smooth = kwargs.get("eigen_smooth", False) + ) - binary_mask = self._get_binary_mask(cam_recovered, threshold) + binary_mask = self._get_binary_mask(cam_recovered, kwargs.get("threshold", "auto")) contours, _ = cv2.findContours(binary_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) @@ -124,7 +321,7 @@ def display_bbox_on_image(self, img_path: str, threshold: float | str = 'auto', g = int(min(255, 2 * (1 - roi_prob) * 255)) box_color = (r, g, 30) # RGB - cv2.rectangle(annotated_img, (x, y), (x + w, y + h), box_color, thickness) + cv2.rectangle(annotated_img, (x, y), (x + w, y + h), box_color, kwargs.get("thickness", 1)) _draw_label( annotated_img, @@ -134,12 +331,4 @@ def display_bbox_on_image(self, img_path: str, threshold: float | str = 'auto', prob_ratio=roi_prob, ) - return annotated_img - - def __repr__(self): - return (f"{self.__class__.__name__}(" - f"model={self.model_name}, dataset={self.dataset}, " - f"margin_ratio={self.margin_ratio}, conf_thres={self.conf_thres}, " - f"branch_level={self.branch_level}, l_stage_idx={self.l_stage_idx}, block_idx={self.block_idx}," - f"aug_smooth={self.aug_smooth}, eigen_smooth={self.eigen_smooth}, " - f"category={self.category},device={self.device})") \ No newline at end of file + return annotated_img \ No newline at end of file