Пример #1
0
    def calculate_pv(self):
        """
        Calculate Pv and return video dict
        """
        logger.debug("Calculating video scores ...")

        # estimate quality from segments
        if 'I13' in self.input_report.keys():
            if 'segments' not in self.input_report["I13"]:
                raise P1203StandaloneError(
                    "No video segments defined, check your input format")

            segments = self.input_report["I13"]["segments"]

            display_res = "1920x1080"
            try:
                display_res = self.input_report["IGen"]["displaySize"]
            except Exception:
                logger.warning(
                    "No display resolution specified, assuming full HD")

            stream_id = None
            try:
                stream_id = self.input_report["I13"]["streamId"]
            except Exception:
                logger.warning("No stream ID specified")

            device = "pc"
            try:
                device = self.input_report["IGen"]["device"]
            except Exception:
                logger.warning(
                    "Device not defined in input report, assuming PC")

            self.video = self.Pv(segments=segments,
                                 display_res=display_res,
                                 device=device,
                                 stream_id=stream_id).calculate()

        # use existing O22 scores
        elif 'O22' in self.input_report.keys():
            self.video = {
                "video": {
                    "streamId": -1,
                    "O22": self.input_report['O22']
                }
            }

        else:
            raise P1203StandaloneError(
                "No 'I13' or 'O22' found in input report")

        if self.debug:
            print(json.dumps(self.video, indent=True, sort_keys=True))

        return self.video
Пример #2
0
 def check_codec(self):
     """ extends the supported codecs
     """
     codecs = list(set([s["codec"] for s in self.segments]))
     for c in codecs:
         if c not in ["h264", "h265", "hevc", "vp9"]:
             raise P1203StandaloneError("Unsupported codec: {}".format(c))
         elif c != "h264":
             if self._show_warning:
                 logger.warning(
                     "Non-standard codec used. O22 Output will not be ITU-T P.1203 compliant."
                 )
         if self.mode != 0 and c != "h264":
             raise P1203StandaloneError(
                 "Non-standard codec calculation only possible with Mode 0."
             )
Пример #3
0
    def model_callback(self, output_sample_timestamp, frames):
        super().model_callback(output_sample_timestamp, frames)
        score = self.o22[-1]
        output_sample_index = [
            i for i, f in enumerate(frames)
            if f["dts"] < output_sample_timestamp
        ][-1]

        # only get the relevant frames from the chunk
        frames = utils.get_chunk(frames, output_sample_index, type="video")

        # non-standard codec mapping
        codec_list = list(set([f["codec"] for f in frames]))
        if len(codec_list) > 1:
            raise P1203StandaloneError(
                "Codec switching between frames in measurement window detected."
            )
        elif codec_list[0] != "h264":

            def correction_func(x, a, b, c, d):
                return a * x * x * x + b * x * x + c * x + d

            if codec_list[0] in ["hevc", "h265"]:
                coeffs = self.COEFFS_H265
            elif codec_list[0] == "vp9":
                coeffs = self.COEFFS_VP9
            else:
                logger.error(
                    "Unsupported codec in measurement window: {}".format(
                        codec_list[0]))
            # compensate score
            score = max(1, min(correction_func(score, *coeffs), 5))

        self.o22[-1] = score
Пример #4
0
    def video_model_function_mode2(coding_res, display_res, framerate, frames, quant=None, avg_qp_per_noni_frame=[]):
        """
        Mode 2 model

        Arguments:
            coding_res {int} -- number of pixels in coding resolution
            display_res {int} -- number of display resolution pixels
            framerate {float} -- frame rate
            frames {list} -- frames
            quant {float} -- quant parameter, only used for debugging [default: None]
            avg_qp_per_noni_frame {list} -- average QP per non-I frame, only used for debugging [default: []]
        Returns:
            float -- O22 score
        """

        if not quant:
            if not avg_qp_per_noni_frame:
                types = []
                qp_values = []
                for frame in frames:
                    qp_values.append(frame["qpValues"])
                    frame_type = frame["type"]
                    if frame_type not in ["I", "P", "B", "Non-I"]:
                        raise P1203StandaloneError("frame type " + str(frame_type) + " not valid; must be I/P/B or I/Non-I")
                    types.append(frame_type)

                qppb = []
                for index, frame_type in enumerate(types):
                    if frame_type in ["P", "B", "Non-I"]:
                        qppb.extend(qp_values[index])
                avg_qp = np.mean(qppb)
            else:
                avg_qp = np.mean(avg_qp_per_noni_frame)
            quant = avg_qp / 51.0

        mos_cod_v = P1203Pv.VIDEO_COEFFS[0] + P1203Pv.VIDEO_COEFFS[1] * math.exp(P1203Pv.VIDEO_COEFFS[2] * quant)
        mos_cod_v = max(min(mos_cod_v, 5), 1)
        deg_cod_v = 100 - utils.r_from_mos(mos_cod_v)
        deg_cod_v = max(min(deg_cod_v, 100), 0)

        # scaling, framerate degradation
        deg_scal_v = P1203Pv.degradation_due_to_upscaling(coding_res, display_res)
        deg_frame_rate_v = P1203Pv.degradation_due_to_frame_rate_reduction(deg_cod_v, deg_scal_v, framerate)

        # degradation integration
        score = P1203Pv.degradation_integration(mos_cod_v, deg_cod_v, deg_scal_v, deg_frame_rate_v)

        logger.debug(json.dumps({
            'coding_res': round(coding_res, 2),
            'display_res': round(display_res, 2),
            'framerate': round(framerate, 2),
            'quant': round(quant, 2),
            'mos_cod_v': round(mos_cod_v, 2),
            'deg_cod_v': round(deg_cod_v, 2),
            'deg_scal_v': round(deg_scal_v, 2),
            'deg_frame_rate_v': round(deg_frame_rate_v, 2),
            'score': round(score, 2)
        }, indent=True))

        return score
Пример #5
0
    def model_callback(self, output_sample_timestamp, frames):
        """
        Function that receives frames from measurement window, to call the model
        on and produce scores.

        Arguments:
            output_sample_timestamp {int} -- timestamp of the output sample (1, 2, ...)
            frames {list} -- list of all frames from measurement window
        """
        logger.debug("Output score at timestamp " +
                     str(output_sample_timestamp))
        output_sample_index = [
            i for i, f in enumerate(frames)
            if f["dts"] < output_sample_timestamp
        ][-1]

        # only get the relevant frames from the chunk
        frames = utils.get_chunk(frames, output_sample_index, type="video")

        first_frame = frames[0]
        if self.mode == 0:
            # average the bitrate for all of the segments
            bitrate = np.mean([f["bitrate"] for f in frames])
            score = self.video_model_function_mode0(
                utils.resolution_to_number(first_frame["resolution"]),
                utils.resolution_to_number(self.display_res), bitrate,
                first_frame["fps"])
        elif self.mode == 1:
            # average the bitrate based on the frame sizes, as implemented
            # in submitted model code
            compensated_sizes = [
                utils.calculate_compensated_size(f["type"], f["size"],
                                                 f["dts"]) for f in frames
            ]
            duration = np.sum([f["duration"] for f in frames])
            bitrate = np.sum(compensated_sizes) * 8 / duration / 1000
            score = self.video_model_function_mode1(
                utils.resolution_to_number(first_frame["resolution"]),
                utils.resolution_to_number(self.display_res), bitrate,
                first_frame["fps"], frames)
        elif self.mode == 2:
            score = self.video_model_function_mode2(
                utils.resolution_to_number(first_frame["resolution"]),
                utils.resolution_to_number(self.display_res),
                first_frame["fps"], frames)
        elif self.mode == 3:
            score = self.video_model_function_mode3(
                utils.resolution_to_number(first_frame["resolution"]),
                utils.resolution_to_number(self.display_res),
                first_frame["fps"], frames)
        else:
            raise P1203StandaloneError("Unsupported mode: {}".format(
                self.mode))

        # mobile adjustments
        if self.device in ["mobile", "handheld"]:
            score = self.handheld_adjustment(score)

        self.o22.append(score)
Пример #6
0
 def check_codec(self):
     """ check if the segments are using valid codecs,
         in P1203 only h264 is allowed
     """
     codecs = list(set([s["codec"] for s in self.segments]))
     for c in codecs:
         if c != "h264":
             raise P1203StandaloneError("Unsupported codec: {}".format(c))
Пример #7
0
def resolution_to_number(string):
    """
    Returns the number of pixels for a resolution given as "wxh", e.g. "1920x1080"
    """
    try:
        return int(string.split("x")[0]) * int(string.split("x")[1])
    except Exception as e:
        raise P1203StandaloneError("Wrong specification of resolution {string}: {e}".format(**locals()))
Пример #8
0
    def audio_model_function(self, codec, bitrate):
        """
        Calculate MOS value based on codec and bitrate.

        - codec: used audio codec, must be one of mp2, ac3, aaclc, heaac
        - bitrate: used audio bitrate in kBit/s
        """
        if codec not in self.VALID_CODECS:
            raise P1203StandaloneError(
                "Unsupported audio codec {}, use any of {}".format(
                    codec, self.VALID_CODECS))

        q_cod_a = self.COEFFS_A1[codec] * math.exp(
            self.COEFFS_A2[codec] * bitrate) + self.COEFFS_A3[codec]
        qa = 100 - q_cod_a
        mos_audio = utils.mos_from_r(qa)
        return mos_audio
Пример #9
0
def get_chunk_hash(frame, type="video"):
    """
    Return a hash value that uniquely identifies a given frame belonging to
    a quality level. This is determined by the frame having a "representation"
    key. If it does not, a quality level is composed of bitrate, codec, fps.
    For audio, only bitrate counts.

    Arguments:
        type {str} -- video or audio

    Returns:
        str -- representation ID or hash of the quality level
    """
    if "representation" in frame.keys():
        return frame["representation"]
    if type == "video":
        return str(frame["bitrate"]) + str(frame["codec"]) + str(frame["fps"])
    elif type == "audio":
        return str(frame["bitrate"]) + str(frame["codec"])
    else:
        raise P1203StandaloneError("Wrong type for frame: " + str(type))
Пример #10
0
    def calculate_pa(self):
        """
        Calculate Pa and return audio dict
        """
        logger.debug("Calculating audio scores ...")

        # estimate quality from segments
        if 'I11' in self.input_report.keys():
            segments = []
            if 'segments' not in self.input_report['I11']:
                logger.warning("No audio segments specified")
            else:
                segments = self.input_report['I11']["segments"]

            stream_id = None
            try:
                stream_id = self.input_report["I11"]["streamId"]
            except Exception:
                logger.warning("No stream ID specified")

            self.audio = self.Pa(segments, stream_id).calculate()

        # use existing O21 scores
        elif 'O21' in self.input_report.keys():
            self.audio = {
                "audio": {
                    "streamId": -1,
                    "O21": self.input_report['O21']
                }
            }

        else:
            raise P1203StandaloneError(
                "No 'I11' or 'O21' found in input report")

        if self.debug:
            print(json.dumps(self.audio, indent=True, sort_keys=True))

        return self.audio
Пример #11
0
def extract_from_single_file(input_file,
                             mode,
                             debug=False,
                             only_pa=False,
                             only_pv=False,
                             print_intermediate=False,
                             modules={},
                             quiet=False):
    """
    Extract the report based on a single input file (JSON or video)

    Arguments:
        input_file {str} -- input file (JSON or video file, or STDIN if "-")
        mode {int} -- 0, 1, 2, 3 depending on extraction mode wanted
        debug {bool} -- whether to run in debug mode
        only_pa {bool} -- only run Pa module
        only_pv {bool} -- only run Pv module
        print_intermediate {bool} -- print intermediate O.21/O.22 values
        modules {dict} -- you can specify Pa, Pv, Pq classnames, that will be used
                          default are the P1203 modules, e.g. modules={"Pa": OtherPaModule}
        quiet {bool} -- Squelch logger messages
    """
    if input_file != "-" and not os.path.isfile(input_file):
        raise P1203StandaloneError(
            "No such file: {input_file}".format(input_file=input_file))

    if input_file == "-":
        stdin = sys.stdin.read()
        input_report = json.loads(stdin)
    else:
        file_ext = os.path.splitext(input_file)[1].lower()[1:]
        valid_video_exts = ["avi", "mp4", "mkv", "nut", "mpeg", "mpg"]

        # normal case, handle JSON files
        if file_ext == "json":
            input_report = utils.read_json_without_comments(input_file)
        # convert input video to required format
        elif file_ext in valid_video_exts:
            logger.debug(
                "Running extract_from_segment_files to get input report: {} mode {}"
                .format(input_file, mode))
            try:
                input_report = Extractor([input_file], mode).extract()
            except Exception as e:
                raise P1203StandaloneError(
                    "Could not auto-generate input report, error: {e.output}".
                    format(e=e))
        else:
            raise P1203StandaloneError(
                "Could not guess what kind of input file this is: {input_file}"
                .format(input_file=input_file))

    # create model ...
    itu_p1203 = P1203Standalone(input_report,
                                debug,
                                Pa=modules.get("Pa", None),
                                Pv=modules.get("Pv", None),
                                Pq=modules.get("Pq", None),
                                quiet=quiet)

    # ... and run it
    if only_pa:
        output = itu_p1203.calculate_pa()
    elif only_pv:
        output = itu_p1203.calculate_pv()
    else:
        output = itu_p1203.calculate_complete(print_intermediate)

    return (input_file, output)
Пример #12
0
    def calculate(self):
        """
        Calculate video MOS

        Returns:
            dict {
                "video": {
                    "streamId": i13["streamId"],
                    "mode": mode,
                    "O22": o22,
                }
            }
        """

        utils.check_segment_continuity(self.segments, "video")

        measurementwindow = MeasurementWindow()
        measurementwindow.set_score_callback(self.model_callback)

        # check which mode can be run
        # TODO: make this switchable by command line option
        self.mode = 0
        for segment in self.segments:
            if "frames" not in segment.keys():
                self.mode = 0
                break
            if "frames" in segment:
                for frame in segment["frames"]:
                    if "frameType" not in frame.keys(
                    ) or "frameSize" not in frame.keys():
                        raise P1203StandaloneError(
                            "Frame definition must have at least 'frameType' and 'frameSize'"
                        )
                    if "qpValues" in frame.keys():
                        self.mode = 3
                    else:
                        self.mode = 1
                        break

        logger.debug("Evaluating stream in mode " + str(self.mode))

        # check for differing or wrong codecs
        self.check_codec()

        # generate fake frames
        if self.mode == 0:
            dts = 0
            for segment in self.segments:
                num_frames = int(segment["duration"] * segment["fps"])
                frame_duration = 1.0 / segment["fps"]
                for i in range(int(num_frames)):
                    frame = {
                        "duration": frame_duration,
                        "dts": dts,
                        "bitrate": segment["bitrate"],
                        "codec": segment["codec"],
                        "fps": segment["fps"],
                        "resolution": segment["resolution"]
                    }
                    if "representation" in segment.keys():
                        frame.update(
                            {"representation": segment["representation"]})
                    # feed frame to MeasurementWindow
                    measurementwindow.add_frame(frame)
                    dts += frame_duration
            measurementwindow.stream_finished()

        # use frame info to infer frames and their DTS, add frame stats
        else:
            dts = 0
            for segment_index, segment in enumerate(self.segments):
                num_frames_assumed = int(segment["duration"] * segment["fps"])
                num_frames = len(segment["frames"])
                if num_frames != num_frames_assumed:
                    logger.warning(
                        "Segment specifies " + str(num_frames) +
                        " frames but based on calculations, there should be " +
                        str(num_frames_assumed))
                frame_duration = 1.0 / segment["fps"]
                for i in range(int(num_frames)):
                    frame = {
                        "duration": frame_duration,
                        "dts": dts,
                        "bitrate": segment["bitrate"],
                        "codec": segment["codec"],
                        "fps": segment["fps"],
                        "resolution": segment["resolution"],
                        "size": segment["frames"][i]["frameSize"],
                        "type": segment["frames"][i]["frameType"],
                    }
                    if "representation" in segment.keys():
                        frame.update(
                            {"representation": segment["representation"]})
                    if self.mode == 3:
                        qp_values = segment["frames"][i]["qpValues"]
                        if not qp_values:
                            raise P1203StandaloneError(
                                "No QP values for frame {i} of segment {segment_index}"
                                .format(**locals()))
                        frame["qpValues"] = qp_values
                    # feed frame to MeasurementWindow
                    measurementwindow.add_frame(frame)
                    dts += frame_duration
            measurementwindow.stream_finished()

        return {
            "video": {
                "streamId": self.stream_id,
                "mode": self.mode,
                "O22": self.o22,
            }
        }
Пример #13
0
    def video_model_function_mode3(self,
                                   coding_res,
                                   display_res,
                                   framerate,
                                   frames,
                                   quant=None,
                                   avg_qp_per_noni_frame=[]):
        """
        Mode 3 model

        Arguments:
            coding_res {int} -- number of pixels in coding resolution
            display_res {int} -- number of display resolution pixels
            framerate {float} -- frame rate
            frames {list} -- frames
            quant {float} -- quant parameter, only used for debugging [default: None]
            avg_qp_per_noni_frame {list} -- average QP per non-I frame, only used for debugging [default: []]
        Returns:
            float -- O22 score
        """

        if not quant:
            # iterate through all frames and collect information
            if not avg_qp_per_noni_frame:
                types = []
                qp_values = []
                for frame in frames:
                    qp_values.append(frame["qpValues"])
                    frame_type = frame["type"]
                    if frame_type not in ["I", "P", "B", "Non-I"]:
                        raise P1203StandaloneError(
                            "frame type " + str(frame_type) +
                            " not valid; must be I/P/B or I/Non-I")
                    types.append(frame_type)

                qppb = []
                for index, frame_type in enumerate(types):
                    if frame_type in ["P", "B", "Non-I"]:
                        qppb.extend(qp_values[index])
                    elif frame_type == "I" and len(qppb) > 0:
                        if len(qppb) > 1:
                            # replace QP value of last P-frame before I frame with QP value of previous P-frame if there
                            # are more than one stored P frames
                            qppb[-1] = qppb[-2]
                        else:
                            # if there is only one stored P frame before I-frame, remove it
                            qppb = []
                avg_qp = np.mean(qppb)
            else:
                avg_qp = np.mean(avg_qp_per_noni_frame)
            quant = avg_qp / 51.0

        q1 = self.coeffs["q1"]
        q2 = self.coeffs["q2"]
        q3 = self.coeffs["q3"]

        mos_cod_v = q1 + q2 * math.exp(q3 * quant)
        mos_cod_v = max(min(mos_cod_v, 5), 1)
        deg_cod_v = 100 - utils.r_from_mos(mos_cod_v)
        deg_cod_v = max(min(deg_cod_v, 100), 0)

        # scaling, framerate degradation
        deg_scal_v = self.degradation_due_to_upscaling(coding_res, display_res)
        deg_frame_rate_v = self.degradation_due_to_frame_rate_reduction(
            deg_cod_v, deg_scal_v, framerate)

        # degradation integration
        score = self.degradation_integration(mos_cod_v, deg_cod_v, deg_scal_v,
                                             deg_frame_rate_v)

        logger.debug(
            json.dumps(
                {
                    'coding_res': round(coding_res, 2),
                    'display_res': round(display_res, 2),
                    'framerate': round(framerate, 2),
                    'quant': round(quant, 2),
                    'mos_cod_v': round(mos_cod_v, 2),
                    'deg_cod_v': round(deg_cod_v, 2),
                    'deg_scal_v': round(deg_scal_v, 2),
                    'deg_frame_rate_v': round(deg_frame_rate_v, 2),
                    'score': round(score, 2)
                },
                indent=True))

        return score
Пример #14
0
    def calculate(self):
        """
        Calculate O46 and other diagnostic values according to P.1203.3

        Returns a dict:
            {
                "O23": O23,
                "O34": O34.tolist(),
                "O35": float(O35),
                "O46": float(O46)
            }
        """
        # ---------------------------------------------------------------------
        # Clause 3.2.2
        O21_len = len(self.O21)
        O22_len = len(self.O22)

        if not self.has_video:
            raise P1203StandaloneError(
                "O22 has no scores; Pq model is not valid without video.")

        if not self.has_audio:
            duration = O22_len
            logger.warning(
                "O21 has no scores, will assume constant high quality audio.")
            self.O21 = np.full(duration, 5.0)
        else:
            # else truncate the duration to the shorter of both streams
            if O21_len > O22_len:
                duration = O22_len
            else:
                duration = O21_len

        # ---------------------------------------------------------------------
        # Clause 8.1.1.1

        # calculate weighted total stalling length
        total_stall_len = sum([
            l_buff *
            utils.exponential(1, self.coeffs["c_ref7"], 0,
                              self.coeffs["c_ref8"], duration - p_buff)
            for p_buff, l_buff in zip(self.p_buff, self.l_buff)
        ])

        # calculate average stalling interval
        avg_stall_interval = 0
        num_stalls = len(self.l_buff)
        if num_stalls > 1:
            avg_stall_interval = sum([
                b - a for a, b in zip(self.p_buff, self.p_buff[1:])
            ]) / (len(self.l_buff) - 1)

        # ---------------------------------------------------------------------
        # Clause 8.1.2.2
        vid_qual_spread = max(self.O22) - min(self.O22)

        # ---------------------------------------------------------------------
        # Clause 8.1.2.3
        vid_qual_change_rate = float(0)
        for i in range(1, duration):
            diff = self.O22[i] - self.O22[i - 1]
            if diff > 0.2 or diff < -0.2:
                vid_qual_change_rate += 1
        vid_qual_change_rate = vid_qual_change_rate / duration

        # ---------------------------------------------------------------------
        # Clause 8.1.2.4 and 8.1.2.5
        QC = []

        ma_order = 5
        ma_kernel = np.ones(ma_order) / ma_order
        padding_beg = np.asarray([self.O22[0]] * (ma_order - 1))
        padding_end = np.asarray([self.O22[-1]] * (ma_order - 1))
        padded_O22 = np.append(np.append(padding_beg, self.O22), padding_end)
        ma_filtered = signal.convolve(padded_O22, ma_kernel,
                                      mode='valid').tolist()

        step = 3
        for current_score, next_score in zip(ma_filtered[0::step],
                                             ma_filtered[step::step]):
            thresh = 0.2
            if (next_score - current_score) > thresh:
                QC.append(1)
            elif -thresh < (next_score - current_score) < thresh:
                QC.append(0)
            else:
                QC.append(-1)

        lens = []
        for index, val in enumerate(QC):
            if val != 0:
                if lens and lens[-1][1] != val:
                    lens.append([index, val])
                if not lens:
                    lens.append([index, val])
        if lens:
            lens.insert(0, [0, 0])
            lens.append([len(QC), 0])
            distances = [b[0] - a[0] for a, b in zip(lens, lens[1:])]
            longest_period = max(distances) * step
        else:
            longest_period = len(QC) * step
        q_dir_changes_longest = longest_period

        q_dir_changes_tot = sum(1
                                for k, g in groupby([s for s in QC if s != 0]))

        # ---------------------------------------------------------------------
        # Eq. 19-21
        O35_denominator = O35_numerator = 0
        O34 = np.zeros(duration)
        for t in range(duration):
            O34[t] = np.maximum(
                np.minimum(
                    self.coeffs["av1"] + self.coeffs["av2"] * self.O21[t] +
                    self.coeffs["av3"] * self.O22[t] +
                    self.coeffs["av4"] * self.O21[t] * self.O22[t], 5), 1)
            temp = O34[t]
            w1 = self.coeffs["t1"] + self.coeffs["t2"] * np.exp(
                (t / float(duration)) / self.coeffs["t3"])
            w2 = self.coeffs["t4"] - self.coeffs["t5"] * temp

            O35_numerator += w1 * w2 * temp
            O35_denominator += w1 * w2
        O35_baseline = O35_numerator / O35_denominator

        # ---------------------------------------------------------------------
        # Clause 8.1.2.1
        O34_diff = list(O34)
        for i in range(duration):
            # Eq. 5
            w_diff = utils.exponential(1, self.coeffs["c1"], 0,
                                       self.coeffs["c2"], duration - i - 1)
            O34_diff[i] = (O34[i] - O35_baseline) * w_diff

        # Eq. 6
        neg_perc = np.percentile(O34_diff, 10, interpolation='linear')
        # Eq. 7
        negative_bias = np.maximum(0, -neg_perc) * self.coeffs["c23"]

        # ---------------------------------------------------------------------
        # Eq. 29
        stalling_impact = np.exp(- num_stalls / self.coeffs["s1"]) * \
            np.exp(- total_stall_len / duration / self.coeffs["s2"]) * \
            np.exp(- avg_stall_interval / duration / self.coeffs["s3"])
        # Eq. 31
        O23 = 1 + 4 * stalling_impact

        # ---------------------------------------------------------------------
        # Clause 8.3

        # Eq. 24
        osc_comp = 0
        osc_test = ((q_dir_changes_longest / duration) <
                    0.25) and (q_dir_changes_longest < 30)
        if osc_test:
            # Eq. 27
            q_diff = np.maximum(0.0, 1 + np.log10(vid_qual_spread + 0.001))
            # Eq. 23
            osc_comp = np.maximum(
                0.0,
                np.minimum(
                    q_diff * np.exp(self.coeffs["comp1"] * q_dir_changes_tot +
                                    self.coeffs["comp2"]), 1.5))

        # Eq. 26
        adapt_comp = 0
        adapt_test = (q_dir_changes_longest / duration) < 0.25
        if adapt_test:
            adapt_comp = np.maximum(
                0.0,
                np.minimum(
                    self.coeffs["comp3"] * vid_qual_spread *
                    vid_qual_change_rate + self.coeffs["comp4"], 0.5))

        # Eq. 18
        O35 = O35_baseline - negative_bias - osc_comp - adapt_comp

        # ---------------------------------------------------------------------
        # Eq. 28
        mos = 1.0 + (O35 - 1.0) * stalling_impact

        # ---------------------------------------------------------------------
        # Eq. 28
        rf_score = rfmodel.calculate(self.O21, self.O22, self.l_buff,
                                     self.p_buff, duration)
        O46 = 0.75 * np.maximum(np.minimum(mos, 5), 1) + 0.25 * rf_score

        return {
            "O23": O23,
            "O34": O34.tolist(),
            "O35": float(O35),
            "O46": float(O46)
        }
Пример #15
0
    def calculate(self):
        """
        Calculate O46 and other diagnostic values according to P.1203.3

        Returns a dict:
            {
                "O23": O23,
                "O34": O34.tolist(),
                "O35": float(O35),
                "O46": float(O46)
            }
        """
        # ---------------------------------------------------------------------
        # Clause 3.2.2
        O21_len = len(self.O21)
        O22_len = len(self.O22)

        if not self.has_video:
            raise P1203StandaloneError(
                "O22 has no scores; Pq model is not valid without video.")

        if not self.has_audio:
            duration = O22_len
            logger.warning(
                "O21 has no scores, will assume constant high quality audio.")
            self.O21 = np.full(duration, 5.0)
        else:
            # else truncate the duration to the shorter of both streams
            if O21_len > O22_len:
                duration = O22_len
            else:
                duration = O21_len

        total_stall_len, num_stalls, avg_stall_interval = self._calc_stalling_features(
            duration)

        # ---------------------------------------------------------------------
        # Clause 8.1.2.2
        vid_qual_spread = max(self.O22) - min(self.O22)

        # ---
        vid_qual_change_rate = self._calc_video_quality_change_rate(duration)

        # ---------------------------------------------------------------------
        q_dir_changes_longest, q_dir_changes_tot = self._calc_qdir()

        # ---------------------------------------------------------------------
        O34, O35_baseline = self._calc_034_035_baseline(duration)

        # ---------------------------------------------------------------------
        # Clause 8.1.2.1
        O34_diff = list(O34)
        for i in range(duration):
            # Eq. 5
            w_diff = utils.exponential(1, self.coeffs["c1"], 0,
                                       self.coeffs["c2"], duration - i - 1)
            O34_diff[i] = (O34[i] - O35_baseline) * w_diff

        # Eq. 6
        neg_perc = np.percentile(O34_diff, 10, interpolation='linear')
        # Eq. 7
        negative_bias = np.maximum(0, -neg_perc) * self.coeffs["c23"]

        stalling_impact = self._calc_stalling_impact(num_stalls,
                                                     total_stall_len, duration,
                                                     avg_stall_interval)
        # Eq. 31
        O23 = 1 + 4 * stalling_impact

        osc_comp = self._calc_and_test_osc(duration, q_dir_changes_longest,
                                           q_dir_changes_tot, vid_qual_spread)

        # Eq. 26
        adapt_comp = 0
        adapt_test = (q_dir_changes_longest / duration) < 0.25
        if adapt_test:
            adapt_comp = np.maximum(
                0.0,
                np.minimum(
                    self.coeffs["comp3"] * vid_qual_spread *
                    vid_qual_change_rate + self.coeffs["comp4"], 0.5))

        # Eq. 18
        O35 = O35_baseline - negative_bias - osc_comp - adapt_comp

        # ---------------------------------------------------------------------
        # Eq. 28
        mos = 1.0 + (O35 - 1.0) * stalling_impact

        # ---------------------------------------------------------------------
        # Eq. 28
        rf_score = rfmodel.calculate(self.O21, self.O22, self.l_buff,
                                     self.p_buff, duration)
        O46 = 0.75 * np.maximum(np.minimum(mos, 5), 1) + 0.25 * rf_score

        if self.amendment_1_stalling:
            q_fac = min(
                max(
                    self.coeffs["amd_1_a1"] * total_stall_len +
                    self.coeffs["amd_1_a2"], 0), 1)
            O46 = 1 + (O46 - 1) * q_fac

        # Eq. 30
        O46 = self.coeffs["f1"] + self.coeffs["f2"] * O46

        return {
            "O23": O23,
            "O34": O34.tolist(),
            "O35": float(O35),
            "O46": float(O46)
        }