이미지 오차 확산의 이치화~ 가마값 고려 Ver.+~

개요


오차 확산을 통해 이미지를 이치화하면 이미지가 밝아 보입니다.
가마를 고려하지 않아 생긴 현상이다.
최근 몇 년 동안 2치 디스플레이 유형의 OLED의 보급으로 인해 이미지를 예쁘게 보여주고 싶은 장면이 자주 발생했다.그래서 예뻐 보여야 한다.
(주요 보급 목적지는 라즈베리피, 비글본, 아르두노, 나노피)
그림% 1개의 캡션을 편집했습니다.

지붕

기존 문제


Python에서 PIL 라이브러리를 사용하면 오차 확산법으로 2치 이미지를 간단하게 생성할 수 있습니다.
예:image = Image.open(cover_path).convert('1')하지만 두 가지 문제가 있다.
  • RGB → Grayscale화는 Bt.601 계열에서 진행됩니다
  • 가마의 값을 고려하지 않는다. 오차가 확산되어 이치화되기 때문이다
  • 최근의 모니터는 sRGB(Bt.709)계이기 때문에 Bt.601의 계수로 그레이스케일링을 할 때 미묘한 불협화감을 일으킬 수 있다.
    감마값을 고려하지 않기 때문에 오차의 확산이 밝아졌다.

    대책 내용


    float는 기본적으로 느리고 모두 고정 소수점의 정수 연산으로 실시한다.

    (1) RGB → Grayscale 변환에서 Bt.709의 계수를 사용합니다.


    Bt.709의 변환은 다음과 같습니다.
    $ Luminance = R * 0.2126 + G * 0.7152 + B * 0.0722 $
    이것은 65536을 곱하여 고정 소수점으로 처리한다.
    이렇게 된다는 거야.
    $ Luminance = (R * 13933 + G * 46871 + B * 4732)/65536 $
    참고로 Bt.601은 아래와 같습니다.
    OpenCV와 PIL의 Grayscale화는 이쪽 식이다.아마도 JPEG의 YUV-RGB 변환이 이렇기 때문일 것이다.
    $ Luminance = R * 0.299 + G * 0.587 + B * 0.114 $

    (2) 오차 확산이 선형 공간에서 실시


    선형 공간으로 돌아가면 계산 오차가 확산됩니다.
    선형 공간을 되돌릴 때 사용하는 가마 값은 sRGB에 대한 근사값 2.2를 사용하고 선형 공간 값은 고정된 소수점 20bit를 나타낸다.
    즉, 가마값이 변환된 후 255는 1048575, 1은 5로 변한다.
    그리고 1을 1로 비추는 것이 최저 조건이기 때문에 Gamma=2.2 시 18bit가 필요하다.
    ※ sRGB의 가마 곡선은 기계 설치를 간소화하기 위해 어두운 부분은 직선으로 다음과 같이 규격화됩니다.
    www.color.org/srgb.pdf
    If R, G, B are less than or equal to 0.04045
      RL = R/12.92
      GL = G/12.92
      BL = B/12.02
    
    If R, G, B are greater than 0.04045
      RL = ((R + 0.055)/1.055)2.4
      GL = ((R + 0.055)/1.055)2.4
      BL = ((R + 0.055)/1.055)2.4
    
    따라서 $(1/255)/12.92*4096=1.2432$이기 때문에 당분간 12bit이면 충분합니다.
    모니터 같은 12bit LUT!!구가하는 것을 기다리는 것은 사실 최저 조건이다.
    ※ 위 규격서의 공식, 오류 3개도 있습니다.

    (3) 오차 확산 FloydSteinberg 사용


    FloydSteinberg
    -
    -
    *
    7/16
    -
    -
    3/16
    5/16
    1/16
    -

    Python 버전 코드


    PIL의 image를 입력하면 PIL의 image를 반환합니다.
    import math
    from PIL import Image
    
    def ImageHalftoning_FloydSteinberg(image):
    
        shift   = 20;
    
        cx, cy = image.size;
    
        temp    = Image.new('I', (cx, cy));
        result  = Image.new('L', (cx, cy));
    
        tmp     = temp.load();
        dst     = result.load();
    
        # Setup Gamma tablw
        gamma   = [0]*256;
        for i in range(256):
            gamma[i]    = int( math.pow( i / 255.0, 2.2 ) * ((1 << shift) - 1) );
    
        # Convert to initial value
        if image.mode == 'L':
            src     = image.load();
            for y in range(cy):
                for x in range(cx):
                    tmp[(x,y)]  =  gamma[ src[(x,y)] ];
    
        elif image.mode == 'RGB':
            src     = image.load();
            for y in range(cy):
                for x in range(cx):
                    R,G,B   = src[(x,y)];
                    Y       = (R * 13933 + G * 46871 + B * 4732) >> 16; # Bt.709
                    tmp[(x,y)]  =  gamma[ Y ];
    
        elif image.mode == 'RGBA':
            src     = image.load();
            for y in range(cy):
                for x in range(cx):
                    R,G,B,A = src[(x,y)];
                    Y       = (R * 13933 + G * 46871 + B * 4732) >> 16; # Bt.709
                    tmp[(x,y)]  =  gamma[ Y ];
    
        else:
            raise ValueError('Image.mode is not supported.')    
    
        # Error diffuse
        for y in range(cy):
            for x in range(cx):
                c   = tmp[(x,y)];
                e   = c if c < (1 << shift) else (c - ((1 << shift) - 1));
    
                dst[(x,y)]  = 0 if c < (1 << shift) else 255;
    
                # FloydSteinberg
                #   -       *       7/16
                #   3/16    5/16    1/16
                if  (x+1) < cx :
                    tmp[(x+1,y)]    += e * 7 / 16;
    
                if (y+1) < cy :
                    if 0 <= (x-1) :
                        tmp[(x-1,y+1)]  += e * 3 / 16;
    
                    tmp[(x,y+1)]        += e * 5 / 16;
    
                    if (x+1) < cx :
                        tmp[(x+1,y+1)]  += e * 1 / 16;
    
        return  result;
    
    C++ 버전의 코드(RGB → L 변환 없음)는 다음 내용을 참조합니다.
    NanoPi-NEO OpenCV Cap으로 OLED 디스플레이 - Qiita

    마지막


    이 Python 코드는 다음과 같은 내용을 적용하기 위해 제작되었습니다.
    코드도git에 등록되었습니다.
    NanoPi-NEO, MPD 및 OLED 음악 재생 서버 - Qiita

    좋은 웹페이지 즐겨찾기