본문 바로가기
Project/어데고 (urdego)

[TensorFlow - NSFW] 부적절한 컨텐츠 감지

by J-rain 2025. 2. 18.

 

이번에는 Urdego 컨텐츠 서비스의 부적절한 컨텐츠 감지에 대한 포스팅을 작성하고자 합니다

 

 

도입 계기

  1. Urdego 프로젝트는 사용자가 생성하는 컨텐츠(UGC)를 게임에 사용하는 특성
  2. 익명성이 보장되는 플랫폼에서 '유해 콘텐츠가 업로드될 가능성'

 

해결방법

  1. Amazon Rekognition (AWS에서 제공하는 부적절한 이미지 및 비디오 감지 API)
  2. Cloud Vision API - SafeSearch (GCP에서 제공하는 유해성 컨텐츠 감지)
  3. Green-eye (Naver에서 제공하는 음란물 차단 AI)
  4. 등등...

찾아보면서 느낀점은 API를 사용하면 손쉽게 구축할 수 있지만 역시 사용에 따른 비용이 발생한다는 점이다..

블로그 포스팅하면서 매번 언급했지만 Urdego는 이미 비용문제 -> 홈서버로 구축했기에 부가적인 비용이 드는 행위(?)를 하고 싶지 않았다.

 

그렇게 API에서 오픈소스로 눈을 돌리고 종일 찾아본 결과..

이미지 처리에 많이 사용되는 TensorFlow가 부적절한 컨텐츠 감지 모델도 있다는 사실을 알아버렸다 👍🏻👍🏻

 

 

TensorFlow - NSFW(Not Safe For Work) ??

NSFW 머신러닝 모델은 이미지를 분석해서 아래 5가지 카테고리 중 하나로 분류하는 역할을 한다.

  1. drawings – 안전한 그림
  2. hentai - 성인 애니메이션
  3. neutral – 일반적인 안전한 이미지
  4. sexy – 선정적이지만 직접적인 포르노는 아닌 이미지
  5. porn – 실제 포르노 이미지 및 성행위

 

참고 깃헙 링크

https://github.com/tensorflow/java

https://github.com/GantMan/nsfw_model/tree/master?tab=readme-ov-file

 

GitHub - GantMan/nsfw_model: Keras model of NSFW detector

Keras model of NSFW detector. Contribute to GantMan/nsfw_model development by creating an account on GitHub.

github.com

 

 

우선 모델이 잘 작동되는지 'Colab' 에서 테스트사진을 업로드하여 인덱스별 비율을 추출해보자

import tensorflow as tf
import numpy as np
from PIL import Image

# 모델 로드
with tf.io.gfile.GFile("nsfw.pb", "rb") as f:
    graph_def = tf.compat.v1.GraphDef()
    graph_def.ParseFromString(f.read())


# 그래프 생성
with tf.Graph().as_default() as graph:
    tf.compat.v1.import_graph_def(graph_def, name="")

# 입력과 출력 노드 이름
input_node = "input_1"
output_node = "dense_3/Softmax"

# 디버깅: 노드 확인
print("Input Node:", input_node)
print("Output Node:", output_node)

# 이미지 전처리
def preprocess_image(image_path):
    img = Image.open(image_path).convert("RGB").resize((224, 224))
    img = np.array(img).astype(np.float32) / 255.0  # float32로 변환
    img = np.expand_dims(img, axis=0)
    print("Preprocessed Image Shape:", img.shape)
    return img

# 테스트 이미지 경로
image_path = "/content/테스트사진1.jpeg"
input_data = preprocess_image(image_path)

# 세션 실행
with tf.compat.v1.Session(graph=graph) as sess:
    try:
        # 입력과 출력 텐서
        input_tensor = graph.get_tensor_by_name(input_node + ":0")
        output_tensor = graph.get_tensor_by_name(output_node + ":0")

        # 모델 실행
        output = sess.run(output_tensor, feed_dict={input_tensor: input_data})


        for i, prob in enumerate(output[0]):  # 배치 내 첫 번째 결과 사용
            print(f"Class {i}: Probability = {prob}")
    except Exception as e:
        print(f"Error during session run: {e}")

 

풍경사진이

[Index 3] neutral – 일반적인 안전한 이미지  0.99 로 잘 추출된 모습이다.

 

내가만든 스콘

마찬가지로

[Index 3] neutral – 일반적인 안전한 이미지  0.99 로 잘 추출된 모습이다.

 

스키장에서 만난 애기

 

사람이 있어도 마찬가지로

[Index 3] neutral – 일반적인 안전한 이미지  0.96 으로 잘 추출된 모습이다.

 

 

19금 사진도 확인해보자,,

 

[Index 4] porn – 실제 포르노 이미지 및 성행위 0.86으로 잘 추출된 모습이다.

 

이렇게 50여가지의 사진들을 테스트한 결과

부적절한 컨텐츠 평균 Index 3, Index 4에서  0.6 이상이 추출되었다.

 

 

이제 내가 할일은 클라이언트에서 컨텐츠 생성 api 요청시 컨텐츠를 분리해서 서버에 저장된 nsfw 모델을 거친 후 결과를 리턴해주면 된다!

 

// 유해 컨텐츠 감지 (NSFW = Not Safe For Work)
public class NSFWDetector {

    // 모델 관련 상수
    private static final Path MODEL_PATH = Paths.get("/urdego/tensorflow/nsfw.pb");
    private static final String TENSOR_INPUT_NAME = "input_1";
    private static final String TENSOR_OUTPUT_NAME = "dense_3/Softmax";
    private static final int TENSOR_OUTPUT_FIRST_INDEX = 0;
    private static final int TENSOR_OUTPUT_CLASSES_INDEX = 1;

    // 이미지 관련 상수
    private static final int IMG_WEIGHT = 224;
    private static final int IMG_HEIGHT = 224;
    private static final double NSFW_THRESHOLD_RATIO = 0.6;
    private static final int EXPLICIT_CLASS_INDEX = 3;
    private static final int PORNO_CLASS_INDEX = 4;
    private static final int BATCH_SIZE = 1;
    private static final int BYTE_MASK = 0xFF;
    private static final int START_X = 0;
    private static final int START_Y = 0;
    private static final int BATCH_INDEX = 0;

    // 색상 관련 상수
    private static final float RGB_MAX_VALUE = 255.0f;
    private static final int RGB_CHANNELS = 3;
    private static final int RED_SHIFT = 16;
    private static final int GREEN_SHIFT = 8;
    private static final int RED_CHANNEL = 0;
    private static final int GREEN_CHANNEL = 1;
    private static final int BLUE_CHANNEL = 2;

    private static final Graph graph;

    static {
        try {
            // 모델 로드
            graph = new Graph();
            byte[] graphDef = Files.readAllBytes(MODEL_PATH);
            graph.importGraphDef(GraphDef.parseFrom(graphDef));
        } catch (IOException e) {
            throw new UserContentException(ExceptionMessage.GRAPH_LOAD_FAILED);
        }
    }

    // 이미지의 NSFW 여부 판단
    public static boolean isNSFW(byte[] imageBytes) throws IOException {
        try (Session session = new Session(graph);
             Tensor inputTensor = preprocessImage(imageBytes)) {

            try (Tensor outputTensor = session.runner()
                    .feed(TENSOR_INPUT_NAME, inputTensor)
                    .fetch(TENSOR_OUTPUT_NAME)
                    .run()
                    .get(TENSOR_OUTPUT_FIRST_INDEX)) {


                try (var rawTensor = outputTensor.asRawTensor()) {
                    // float 데이터 버퍼 가져오기
                    var floatBuffer = rawTensor.data().asFloats();
                    int outputSize = (int) outputTensor.shape().asArray()[TENSOR_OUTPUT_CLASSES_INDEX];
                    float[] probabilities = new float[outputSize];

                    // float 배열로 복사
                    floatBuffer.read(probabilities);

                    // 인덱스 3과 4의 값 확인
                    float nsfwProbabilityClass3 = probabilities[EXPLICIT_CLASS_INDEX]; // NSFW 3 (Explicit NSFW Content)
                    float nsfwProbabilityClass4 = probabilities[PORNO_CLASS_INDEX]; // NSFW 4 (Porno NSFW Content)

                    // NSFW 비율 판단
                    return nsfwProbabilityClass3 > NSFW_THRESHOLD_RATIO || nsfwProbabilityClass4 > NSFW_THRESHOLD_RATIO;
                }
            }
        }
    }

    // 이미지 전처리 Tensor 변환
    private static TFloat32 preprocessImage(byte[] imageBytes) throws IOException {
        BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes));

        // 224x224로 리사이즈 -> 딥러닝 CNN 모델(tensorflow) 사용
        BufferedImage resized = new BufferedImage(IMG_WEIGHT, IMG_HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = resized.createGraphics();
        g.drawImage(img, START_X, START_Y, IMG_WEIGHT, IMG_HEIGHT, null);
        g.dispose();

        float[][][][] inputData = new float[BATCH_SIZE][IMG_WEIGHT][IMG_HEIGHT][RGB_CHANNELS];
        for (int y = 0; y < IMG_WEIGHT; y++) {
            for (int x = 0; x < IMG_HEIGHT; x++) {
                int rgb = resized.getRGB(x, y);
                inputData[BATCH_INDEX][y][x][RED_CHANNEL] = ((rgb >> RED_SHIFT) & BYTE_MASK) / RGB_MAX_VALUE; // Red
                inputData[BATCH_INDEX][y][x][GREEN_CHANNEL] = ((rgb >> GREEN_SHIFT) & BYTE_MASK) / RGB_MAX_VALUE;  // Green
                inputData[BATCH_INDEX][y][x][BLUE_CHANNEL] = (rgb & BYTE_MASK) / RGB_MAX_VALUE;         // Blue
            }
        }

        // Tensor 생성
        return TFloat32.tensorOf(org.tensorflow.ndarray.StdArrays.ndCopyOf(inputData));
    }

}

 

 

 

스웨거 테스트 결과
컨텐츠 서버 log

 

 

굳 👍🏻👍🏻

댓글