手機護眼概論及OLED螢幕降低頻閃原理介紹

cjyyxn發表於2024-06-26

頻閃的度量

目前手機螢幕頻閃的度量主要有兩種方式,一種是用低快門時間的相機拍攝手機螢幕,觀察黑色條紋;另一種是高時間解析度的照度探頭,測出螢幕上指定區域的亮度隨時間變化曲線,再透過一定的公式計算出頻閃效應可見性度量值(SVM, Stroboscopic effect visibility measure)。

相機拍攝

相機拍攝的方式相當簡單,只要有一部手機,就可以觀察頻閃程度。具體方法為,將手機相機調到專業模式,將快門時間調到 1/4000 秒以下,對準被測手機螢幕,然後可以看到黑色條紋,如圖所示

一般來說,黑色條紋越寬、顏色越深、越稀疏,頻閃程度越強。

SVM 計算方法[1]

感測器測得的照度隨時間變化產生波形。將波形歸一化,使時間平均值等於 1 ,得到相對照度波形,記為 \(y(t)\),並進行三角傅立葉級數展開

\[y(t) = \dfrac{a_0}{2} + \sum\limits_{m=1}^{\infty} \left( a_m\cos\left(\dfrac{2\pi m t}{T}\right) + b_m\sin\left(\dfrac{2\pi m t}{T}\right) \right) \]

相對照度波形的第 \(m\) 個傅立葉分量的相對幅度記為 \(C_{m} = \sqrt{a_m^2 + b_m^2}\),頻率記為 \(f_{m} = \dfrac{m}{T}\)

考慮頻閃效應對比度閾值函式(stroboscopic effect contrast threshold function)

\(T_{m}\) 為頻率 \(f_{m}\) 對應的頻閃效應對比度閾值函式值。

則 SVM 計算公式如下

\[SVM=\sqrt[3.7]{\sum\limits_{m=1}^{\infty}\left(\dfrac{C_{m}}{T_{m}}\right)^{3.7}} \]

SVM 值越高,頻閃程度越高。而且 SVM 值是可以進行精確計算的,因此可以把 SVM 作為頻閃分析的理論依據。

觀察 SVM 的計算過程,可以發現其取值與螢幕亮度絕對值無關,只與亮度隨時間變化曲線的形狀有關

亮度越高,頻閃越低

這個結論非常容易驗證。最直接的,B站 up 主低調的山用相機拍攝過大量 OLED 螢幕,都有在高亮度下低頻閃,在低亮度下高頻閃的現象。[2]

更進一步的,up 主 Navis-慢點評測展示了 OLED 手機螢幕 SVM 隨螢幕亮度變化曲線[3]

up 主先看評測製作 app 先看頻閃,同樣展示了 OLED 手機螢幕 SVM 隨螢幕亮度變化曲線[4]

因此可以得出結論,一般情況下,OLED 螢幕亮度越高,頻閃越低。結合分析 SVM 計算過程得到的結論,有降低螢幕頻閃的方法:維持螢幕在高亮度,透過增加一個不透明度可調節的黑色濾鏡來控制螢幕實際亮度,從而實現在低亮度下也有低頻閃,這就是透過螢幕濾鏡降低手機頻閃的原理。

螢幕濾鏡在安卓系統的實現

幸運的是,安卓系統給出了足夠的 api,使我們能夠實現螢幕濾鏡。

首先,app 需要開啟無障礙服務,獲取顯示在整個螢幕上的許可權。

參考: https://developer.android.com/guide/topics/ui/accessibility/service

開啟無障礙服務後,利用無障礙服務的上下文獲取整個螢幕的視窗管理器,往視窗管理器新增純黑色、透明度可調的檢視物件,和相應的引數物件,就實現了螢幕濾鏡。

將無障礙服務上下文傳入下面程式碼的 FilterViewManager 物件,即可在螢幕上顯示一個透明度可調的黑色濾鏡。

import android.content.Context;
import android.graphics.Color;
import android.graphics.PixelFormat;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.WindowManager;

public class FilterViewManager {

    private final Context context;
    private final WindowManager windowManager;
    private final WindowManager.LayoutParams layoutParams;
    private final FilterView filterView;
    /**
     * 濾鏡處於開啟狀態,為 true
     */
    public boolean isOpen;
    private float alpha = 0f;
    private float hardwareBrightness = 0f;

    public FilterViewManager(Context c) {
        // 這裡假設傳入的 Context 有無障礙許可權,後面的程式碼不對無障礙許可權進行檢驗

        isOpen = false;
        context = c;
        windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        layoutParams = new WindowManager.LayoutParams();
        filterView = new FilterView(context);

        layoutParams.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
        // width 和 height 儘可能大,從而覆蓋螢幕
        layoutParams.width = 4000;
        layoutParams.height = 4000;
        layoutParams.format = PixelFormat.TRANSLUCENT;
        layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
                WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
    }

    public void open() {
        new Handler(Looper.getMainLooper()).post(() -> {
            // 在UI執行緒中更新UI元件
            if (!isOpen) {
                windowManager.addView(filterView, layoutParams);
                isOpen = true;
            }
        });
    }

    public void close() {
        new Handler(Looper.getMainLooper()).post(() -> {
            // 在UI執行緒中更新UI元件
            if (isOpen) {
                windowManager.removeView(filterView);
                isOpen = false;
            }
        });
    }

    public float getAlpha() {
        if (isOpen) {
            return alpha;
        } else {
            return -1f;
        }
    }

    public void setAlpha(float alpha) {
        new Handler(Looper.getMainLooper()).post(() -> {
            if (isOpen) {
                float a = Math.min(1f, Math.max(0f, alpha));
                // 在UI執行緒中更新UI元件
                filterView.setAlpha(a);
                this.alpha = a;
            }
        });
    }

    public float getHardwareBrightness() {
        if (isOpen) {
            return hardwareBrightness;
        } else {
            return -1f;
        }
    }

    public void setHardwareBrightness(float brightness) {
        new Handler(Looper.getMainLooper()).post(() -> {
            if (isOpen) {
                float b = Math.min(1f, Math.max(0f, brightness));
                // 在UI執行緒中更新UI元件
                // layoutParams.screenBrightness 會覆蓋系統亮度設定
                layoutParams.screenBrightness = b;
                windowManager.updateViewLayout(filterView, layoutParams);
                hardwareBrightness = b;
            }
        });
    }

    private static class FilterView extends View {

        public FilterView(Context context) {
            super(context);
            setBackgroundColor(Color.BLACK);
            setAlpha(0f);
        }

        @Override
        public void setAlpha(float alpha) {
            super.setAlpha(alpha);
            invalidate();
        }
    }
}

我開發的開源 APP——濾鏡護眼防頻閃

應用簡介:

對於 OLED 螢幕的手機,一般情況下,螢幕亮度越低,頻閃越強。本應用控制螢幕具有較高的亮度,並透過給螢幕新增一層不透明度可調的黑色濾鏡來調節實際亮度,從而實現低亮度下也有低頻閃的效果。

github 專案原始碼: https://github.com/cjyyx/ScreenFilter

下載連結 1:github release

https://github.com/cjyyx/ScreenFilter/releases

下載連結 2:藍奏雲

https://wwis.lanzouq.com/b04whksif

密碼:1234

注意:

  1. 支援直接拖動系統狀態列亮度條來控制亮度
  2. 當環境光照較高時,應用會自動關閉螢幕濾鏡並開啟系統自動亮度,從而使螢幕能夠達到最大激發亮度
  3. 最低支援版本安卓11
  4. 本應用在開發時沒有考慮相容性,目前只能保證在我的手機上正常執行。我的手機系統是 MIUI14
  5. 開啟濾鏡時不要開啟系統紙質護眼,否則會造成破圖

參考資料


  1. 維基百科, https://en.wikipedia.org/wiki/Stroboscopic_effect ↩︎

  2. 低調的山, https://space.bilibili.com/394790691 ↩︎

  3. Navis-慢點評測, https://space.bilibili.com/8986182 ↩︎

  4. 先看頻閃, https://www.bilibili.com/video/BV1K14y1D7mg ↩︎

相關文章