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
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." )
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
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
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)
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))
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()))
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
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))
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
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)
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, } }
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
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) }
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) }