Merge pull request #174 from Zeecka/worket/color_remap

Worket/color remap
This commit is contained in:
Zeecka
2026-01-14 21:31:43 +01:00
committed by GitHub
3 changed files with 118 additions and 6 deletions
+103
View File
@@ -0,0 +1,103 @@
# flake8: noqa: E203,E501,W503
# pylint: disable=C0413,W0718,R0903,R0801,W0612,R0912
# mypy: disable-error-code=unused-awaitable
"""Color Remapping Analyzer for Image Submissions."""
from pathlib import Path
from typing import Any, Optional
import numpy as np
from PIL import Image
from .base_analyzer import SubprocessAnalyzer
class ColorRemappingAnalyzer(SubprocessAnalyzer):
"""Analyzer for color remapping."""
def __init__(self, input_img: Path, output_dir: Path) -> None:
super().__init__("color_remapping", input_img, output_dir)
def _normalize_image(self, img_np: np.ndarray) -> tuple[np.ndarray, int]:
"""Normalize image to have RGB or RGBA channels.
Returns:
Tuple of (normalized image array, number of channels)
"""
if len(img_np.shape) == 2:
channels = 1
img_np = np.expand_dims(img_np, axis=-1)
else:
channels = img_np.shape[2]
if channels < 3:
# Grayscale to RGB
img_np = np.repeat(img_np, 3, axis=2)
channels = 3
elif channels > 4:
# More than 4 channels, take first 3
img_np = img_np[..., :3]
channels = 3
return img_np, channels
def _create_remapped_image(self, img_np: np.ndarray, color_map: np.ndarray) -> Image.Image:
"""Create a single remapped image using the color map."""
remapped = np.zeros_like(img_np)
for c in range(min(3, img_np.shape[2])): # Apply to RGB channels
remapped[..., c] = color_map[img_np[..., c]]
# Keep alpha channel if present
if img_np.shape[2] == 4:
remapped[..., 3] = img_np[..., 3]
remapped_img = Image.fromarray(remapped.astype(np.uint8), mode="RGBA")
else:
remapped_img = Image.fromarray(remapped[..., :3].astype(np.uint8), mode="RGB")
return remapped_img
def get_results(self, _: Optional[str] = None) -> dict[str, Any]:
"""Analyze an image submission using color remapping."""
img = Image.open(self.input_img)
converted = False
if img.mode == "P":
img = img.convert("RGB")
converted = True
img_np = np.array(img)
img_np, _ = self._normalize_image(img_np)
image_json = []
# Generate 8 random color remappings
for i in range(8):
# Create a random permutation of color values (0-255)
color_map = np.random.randint(0, 256, size=256, dtype=np.uint8)
remapped_img = self._create_remapped_image(img_np, color_map)
# Save the remapped image
img_name = f"color_remapping_{i:02d}.png"
out_path = self.output_dir / img_name
dl_path = Path(self.output_dir.name) / img_name
image_json.append("/image/" + str(dl_path))
remapped_img.save(out_path)
output = {
"status": "ok",
"images": {
"Color Remapping": image_json,
},
}
if converted:
output["note"] = (
"Image contains a color palette and was converted to RGB for processing."
)
return output
def analyze_color_remapping(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using color remapping."""
analyzer = ColorRemappingAnalyzer(input_img, output_dir)
analyzer.analyze()
+9 -2
View File
@@ -1,5 +1,6 @@
const TOOL_ORDER = [ const TOOL_ORDER = [
"decomposer", "decomposer",
"color_remapping",
"file", "file",
"exiftool", "exiftool",
"binwalk", "binwalk",
@@ -515,14 +516,19 @@ function parseResult(result) {
// Parse image output // Parse image output
var channels = ["Superimposed", "Red", "Green", "Blue", "Alpha"]; var channels = ["Superimposed", "Red", "Green", "Blue", "Alpha"];
if (Object.keys(result[tool]["images"]).length == 1) { if (Object.keys(result[tool]["images"]).length == 1) {
channels = ["Grayscale"]; channels = Object.keys(result[tool]["images"]);
} else if (Object.keys(result[tool]["images"]).length == 4) { } else if (Object.keys(result[tool]["images"]).length == 4) {
channels = ["Superimposed", "Red", "Green", "Blue"]; channels = ["Superimposed", "Red", "Green", "Blue"];
} }
let title_h3 = "";
for (const channel of channels) { for (const channel of channels) {
const images = result[tool]["images"][channel]; const images = result[tool]["images"][channel];
analyzer.innerHTML += `<h3>${capitalize(escapeHtml(channel))}</h3>`; if (images) {
title_h3 = capitalize(escapeHtml(channel));
if (title_h3 != "Color Remapping"){
analyzer.innerHTML += `<h3>${title_h3}</h3>`;
}
for (const image of images) { for (const image of images) {
analyzer.innerHTML += `<div class='results_img'><img src='${escapeHtml( analyzer.innerHTML += `<div class='results_img'><img src='${escapeHtml(
image image
@@ -530,6 +536,7 @@ function parseResult(result) {
} }
} }
} }
}
if ("image" in result[tool]) { if ("image" in result[tool]) {
// Parse image output // Parse image output
+2
View File
@@ -14,6 +14,7 @@ from .utils.sentry import initialize_sentry
initialize_sentry() initialize_sentry()
from .analyzers.binwalk import analyze_binwalk # pylint: disable=C0413 from .analyzers.binwalk import analyze_binwalk # pylint: disable=C0413
from .analyzers.color_remapping import analyze_color_remapping
from .analyzers.decomposer import analyze_decomposer from .analyzers.decomposer import analyze_decomposer
from .analyzers.exiftool import analyze_exiftool from .analyzers.exiftool import analyze_exiftool
from .analyzers.file import analyze_file from .analyzers.file import analyze_file
@@ -103,6 +104,7 @@ def analyze_image(submission_hash: str) -> None:
analyzers = [ analyzers = [
(analyze_binwalk, img_path, result_path), (analyze_binwalk, img_path, result_path),
(analyze_color_remapping, img_path, result_path),
(analyze_decomposer, img_path, result_path), (analyze_decomposer, img_path, result_path),
(analyze_exiftool, img_path, result_path), (analyze_exiftool, img_path, result_path),
(analyze_file, img_path, result_path), (analyze_file, img_path, result_path),