# Copyright 2024 NXP Semiconductors
# SPDX-License-Identifier: BSD-3-Clause

import sys
import cv2
import numpy as np
import gi
import cairo
import time
import subprocess
from multiprocessing import Queue, Process
import re
import posix_ipc
gi.require_version('Gst', '1.0')
from gi.repository import Gst,GObject,GLib
import tflite_runtime.interpreter as tflite
from queue import Empty

GST_DEBUG=3

class Demo():
    def __init__(self, result_queue, mq_name):
        self.detector0 = MobilenetV3("mobilenetv3_uint8_400_25Q4.tflite", ext_delegate="/usr/lib/libneutron_delegate.so")
        self.detector1 = SSDMobileNetV2("mobilenet_v2_ssd_quant_int8_25Q4.tflite",ext_delegate="/usr/lib/libneutron_delegate.so")
        self.detector2 = SSDMobileNetV2("mobilenet_v2_ssd_quant_int8_25Q4.tflite",ext_delegate="/usr/lib/libneutron_delegate.so")
        self.newcords1 = []
        self.newcords2 = []
        self.lines = []
        self.fps = 0
        self.inference_time = 0
        self.DRAW_LANE = 1
        self.DRAW_ML = 1
        self.labels0 = ["with-helmet","without-helmet"]
        self.labels = labels = ['pedestrian','car','pothole','greenlight','redlight']
        self.result_queue = result_queue 
        self.mq = posix_ipc.MessageQueue(mq_name, flags=posix_ipc.O_CREAT, mode=0o666, max_messages=10, max_message_size=1024)

        cam_pipeline1 = (
                    "libcamerasrc camera-name=/base/soc/bus@42000000/i2c@42530000/max96724@27/i2c-mux/i2c@0/mx95mbcam@40"
                    + " ! video/x-raw,format=NV12 ! imxvideoconvert_ocl video-warp-enable=true video-warp-coord-file=Buffer_63dc20ab1c.bin"
                    + " ! video/x-raw,format=RGB,width=540,height=360"
                    + " ! videocrop top=20 left=14 right=14 bottom=20"
                    + " ! tee name=cam"
                    + " ! videoconvert"
                    + " ! queue max-size-buffers=2 leaky=2"
                    + " ! cairooverlay name=drawer"
                    + " ! queue max-size-buffers=2 leaky=2"
                    + " ! waylandsink window-height=320 window_width=512"
                    + " cam. ! queue max-size-buffers=2 leaky=2"
                    + " ! appsink emit-signals=true drop=true max-buffers=2 name=ml_sink"
                )
        pipeline1 = Gst.parse_launch(cam_pipeline1)
        pipeline1.set_state(Gst.State.PLAYING)
        drawer1 = pipeline1.get_by_name("drawer")
        drawer1.connect("draw", self.draw1)
        ml_sink1 = pipeline1.get_by_name("ml_sink")
        ml_sink1.connect("new-sample", self.inference1)

        cam_pipeline2 = (
                        "libcamerasrc camera-name=/base/soc/bus@42000000/i2c@42530000/max96724@27/i2c-mux/i2c@1/mx95mbcam@40"
                        + " ! video/x-raw,format=NV12 ! imxvideoconvert_ocl video-warp-enable=true video-warp-coord-file=/usr/share/dcc/Buffer_63dc20ab1c.bin"
                        + " ! video/x-raw,format=RGB,width=540,height=360"
                        + " ! videocrop top=20 left=14 right=14 bottom=20"
                        + " ! tee name=cam"
                        + " ! videoconvert"
                        + " ! queue max-size-buffers=2 leaky=2"
                        + " ! cairooverlay name=drawer"
                        + " ! queue max-size-buffers=2 leaky=2"
                        + " ! waylandsink window-height=320 window_width=512"
                        + " cam. ! queue max-size-buffers=2 leaky=2"
                        + " ! appsink emit-signals=true drop=true max-buffers=2 name=ml_sink"
                    )
        pipeline2 = Gst.parse_launch(cam_pipeline2)
        pipeline2.set_state(Gst.State.PLAYING)
        drawer2 = pipeline2.get_by_name("drawer")
        drawer2.connect("draw", self.draw2)
        ml_sink2 = pipeline2.get_by_name("ml_sink")
        ml_sink2.connect("new-sample", self.inference2)

        cam_pipeline0 = (
                    "libcamerasrc camera-name=/base/soc/bus@42000000/i2c@42530000/max96724@27/i2c-mux/i2c@2/mx95mbcam@40"
                    + " ! video/x-raw,format=NV12! imxvideoconvert_ocl video-warp-enable=true video-warp-coord-file=/usr/share/dcc/Buffer_63dc20ab1c.bin"
                    + " ! video/x-raw,format=RGB,width=540,height=360"
                    + " ! videocrop top=20 left=14 right=14 bottom=20"
                    + " ! videoconvert"
                    + " ! queue max-size-buffers=2 leaky=2"
                    + " ! videorate"  
                    + " ! video/x-raw,framerate=1/1"  
                    + " ! videoconvert ! video/x-raw,format=RGB"
                    + " ! appsink emit-signals=true drop=true max-buffers=2 name=ml_sink"
                )
        pipeline0 = Gst.parse_launch(cam_pipeline0)
        pipeline0.set_state(Gst.State.PLAYING)
        ml_sink0 = pipeline0.get_by_name("ml_sink")
        ml_sink0.connect("new-sample", self.inference0)

        print("PIPELINE INIT SUCCESSFULLY!")

    def inference0(self, data):
        """Run model inference on data from gst pipeline"""
        frame = data.emit("pull-sample")
        if frame is None:
            return 0
        buffer = frame.get_buffer()
        caps = frame.get_caps()
        ret, mem_buf = buffer.map(Gst.MapFlags.READ)
        height = caps.get_structure(0).get_value("height")
        width = caps.get_structure(0).get_value("width")
        frame = np.ndarray(
            shape=(height, width, 3), dtype=np.uint8, buffer=mem_buf.data
        )[..., ::-1]
        self.results = self.detector0.inference(frame)
        # print("DMS_self.results=",self.results)
        # print("helmet detection result=",self.labels0[self.results])
        # print("helmet detection result= *** ",self.results)
        self.result_queue.put(self.results)
        # print("self.result_queue=",self.result_queue)
        buffer.unmap(mem_buf)
        return 0

    def lane(self,img):
        low = 300 
        high = 400 
        rho = 1  # Hof pixel unit
        theta = np.pi / 360  # Hough's Angle Movement Steps
        hof_threshold = 20  # Hof Plane Accumulation Threshold
        min_line_len = 10  # Minimum length of line segment
        max_line_gap = 100  # Maximum permissible breaking length
        edges = cv2.Canny(img, low, high)
        mask = np.zeros_like(edges)
        # vertices = np.array( [[(1,364),(160,207),(234,209),(233,208),(356,348)]],dtype=np.int32)#ROI
        vertices = np.array( [[(57,318),(240,170),(326,170),(445,318)]],dtype=np.int32)#ROI
        cv2.fillPoly(mask, vertices, 255)
        masked_edges = cv2.bitwise_and(edges, mask)
        line_image = np.zeros_like(img)
        # draw lines
        self.lines = hough_lines(masked_edges, rho, theta, hof_threshold, min_line_len, max_line_gap)
        return self.lines

    def inference1(self, data):
        # print("[PIPELINE] [CAM1] start new frame...")
        """Run model inference on data from gst pipeline"""
        frame = data.emit("pull-sample")
        if frame is None:
            return 0
        buffer = frame.get_buffer()
        caps = frame.get_caps()
        ret, mem_buf = buffer.map(Gst.MapFlags.READ)
        height = caps.get_structure(0).get_value("height")
        width = caps.get_structure(0).get_value("width")
        frame = np.ndarray(
            shape=(height, width, 3), dtype=np.uint8, buffer=mem_buf.data
        )[..., ::-1]
        # self.lines = self.lane(frame)
        self.inference_time, self.fps, self.results = self.detector1.inference(frame)
        num_result = len(self.results)
        # print("num_of_front_result",num_result)
        self.newcords1 = []
        for i in range(0, num_result):
            left = int(self.results[i][0])
            top = int(self.results[i][1])
            right = int(self.results[i][2])
            bottom = int(self.results[i][3])
            score = self.results[i][4]
            class_id = self.results[i][5]
            class_id = self.labels[class_id]
            print("front_class_id=",class_id)
            self.newcords1.append([left, top, right, bottom, score, class_id])
        buffer.unmap(mem_buf)
        return 0
    
    def inference2(self, data):
        # print("[PIPELINE] [CAM2] start new frame...")
        """Run model inference on data from gst pipeline"""
        frame = data.emit("pull-sample")
        if frame is None:
            return 0
        buffer = frame.get_buffer()
        caps = frame.get_caps()
        ret, mem_buf = buffer.map(Gst.MapFlags.READ)
        height = caps.get_structure(0).get_value("height")
        width = caps.get_structure(0).get_value("width")
        frame = np.ndarray(
            shape=(height, width, 3), dtype=np.uint8, buffer=mem_buf.data
        )[..., ::-1]
        self.inference_time, self.fps, self.results = self.detector2.inference(frame)
        num_result = len(self.results)
        # print("num_of_rear_result",num_result)
        self.newcords2 = []
        for i in range(0, num_result):
            left = int(self.results[i][0])
            top = int(self.results[i][1])
            right = int(self.results[i][2])
            bottom = int(self.results[i][3])
            score = self.results[i][4]
            class_id = self.results[i][5]
            class_id = self.labels[class_id]
            self.newcords2.append([left, top, right, bottom, score, class_id])
        buffer.unmap(mem_buf)
        return 0

    def draw1(self, overlay, context, timestamp, duration):
        context.select_font_face(
            "Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD
        )
        context.set_source_rgb(1, 1, 0)
        context.move_to(20,20)
        context.set_font_size(int(20.0))
        fps = "fps=" + str(self.fps)
        
        context.show_text(fps)
        context.move_to(20,40)
        inference_time = "inference_time=" + str((int)(self.inference_time)) + "ms"
        context.show_text(inference_time)
        context.set_line_width(3)
        if (self.DRAW_ML == 1):
            """Draw the INFERENCE result on the display"""

            for cords in self.newcords1 :
                x1,y1,x2,y2,precision,classes = cords
                width = x2-x1
                height = y2-y1
                if precision > 0.6 or classes == "pothole":
                    context.rectangle(x1,y1,width,height)
                    context.move_to(x1,y1+20)
                    context.set_font_size(int(25.0))
                    context.show_text(str(classes))
                    context.move_to(x1,y1+40)
                    context.set_font_size(int(20.0))
                    context.show_text(str(precision))
                    context.stroke()     

        if (self.DRAW_LANE == 1):
            """Draw the LANE result on the display"""
            context.set_line_width(3)

            right_y_set = []
            right_x_set = []
            right_slope_set = []

            left_y_set = []
            left_x_set = []
            left_slope_set = []

            slope_min = 0.1  # slope threshold min default 0.35
            slope_max = 10  # slope threshold max default 10
            middle_x = 256
            max_y = 320

            if self.lines is None:
                return 0
            lines = self.lines
            for line in lines:
                for x1, y1, x2, y2 in line:
                    if x2 == x1 :
                        return 0
                    # fit = np.polyfit((x1, x2), (y1, y2), 2)
                    # slope = fit[0]
                    
                    slope = (y2 - y1) / (x2 - x1)
                    if slope_min < np.absolute(slope) <= slope_max:

                        # right lane
                        if slope > 0 and x1 > middle_x and x2 > middle_x:
                            right_y_set.append(y1)
                            right_y_set.append(y2)
                            right_x_set.append(x1)
                            right_x_set.append(x2)
                            right_slope_set.append(slope)
                        # left lane
                        elif slope < 0 and x1 < middle_x and x2 < middle_x:
                            left_y_set.append(y1)
                            left_y_set.append(y2)
                            left_x_set.append(x1)
                            left_x_set.append(x2)
                            left_slope_set.append(slope)
            # draw right lane
            fill_set = []
            if right_y_set:
                rindex = right_y_set.index(min(right_y_set))  # highest point
                right_x_top = right_x_set[rindex]
                right_y_top = right_y_set[rindex]
                rslope = np.median(right_slope_set)
                # Calculate the point where the lane line intersects the bottom of the picture
                right_x_bottom = int(right_x_top + (max_y - right_y_top) / rslope)
                # draw lines
                fill_set.append([right_x_top, right_y_top])
                fill_set.append([right_x_bottom, max_y])

            # draw lines
            if left_y_set:
                lindex = left_y_set.index(min(left_y_set))  # highest point
                left_x_top = left_x_set[lindex]
                left_y_top = left_y_set[lindex]
                lslope = np.median(left_slope_set)  
                left_x_bottom = int(left_x_top + (max_y - left_y_top) / lslope)

                fill_set.append([left_x_bottom, max_y])
                fill_set.append([left_x_top, left_y_top])
                

            if (len(fill_set) == 4):
                context.move_to(right_x_top,right_y_top)
                context.line_to(right_x_bottom, max_y)
                context.set_source_rgba(0, 1, 0, 1)
                context.stroke()

                context.move_to(left_x_bottom, max_y)
                context.line_to(left_x_top, left_y_top)
                context.set_source_rgba(0, 1, 0, 1)
                context.stroke()
                
                context.move_to(fill_set[0][0],fill_set[0][1])
                context.line_to(fill_set[1][0],fill_set[1][1])
                context.line_to(fill_set[2][0],fill_set[2][1])
                context.line_to(fill_set[3][0],fill_set[3][1])
                context.close_path()
                context.set_source_rgba(0, 1, 1, 0.5)
                context.fill()

    def draw2(self, overlay, context, timestamp, duration):
        context.select_font_face(
            "Arial", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD
        )
        context.set_source_rgb(1, 1, 0)
        context.move_to(20,20)
        context.set_font_size(int(20.0))
        fps = "fps=" + str(self.fps)
        
        context.show_text(fps)
        context.move_to(20,40)
        inference_time = "inference_time=" + str((int)(self.inference_time)) + "ms"
        context.show_text(inference_time)
        context.set_line_width(3)
        if (self.DRAW_ML == 1):
            """Draw the INFERENCE result on the display"""

            for cords in self.newcords2 :
                x1,y1,x2,y2,precision,classes = cords
                width = x2-x1
                height = y2-y1
                if precision > 0.75 or classes == "pothole":
                    context.rectangle(x1,y1,width,height)
                    context.move_to(x1,y1+20)
                    context.set_font_size(int(25.0))
                    context.show_text(str(classes))
                    context.move_to(x1,y1+40)
                    context.set_font_size(int(20.0))
                    context.show_text(str(precision))
                    context.stroke()     
                
def hough_lines(img, rho, theta, threshold, min_line_len, max_line_gap):
    lines = cv2.HoughLinesP(img, rho, theta, threshold, np.array([]), minLineLength=min_line_len,
                            maxLineGap=max_line_gap)
    return lines


def process_results(result_queue, mq_name):
    print(f"Starting process_results with result_queue id: {id(result_queue)}")
    mq = posix_ipc.MessageQueue(mq_name, flags=posix_ipc.O_CREAT, mode=0o666, max_messages=10, max_message_size=1024)
    while True:
        try:
            class_id = result_queue.get(timeout=1)
            print(f"Got class_id from result_queue: {class_id}")
            if class_id is None:
                break
            print(f"Received class_id: {class_id}")
            try:
                mq.send(str(class_id).encode(),timeout=1)  # Ensure the class_id is a string before encoding
            except posix_ipc.BusyError:
                print("Message queue is full, skipping this message.")
        except Empty:
            print("No messages in the queue, continuing...")
        except KeyboardInterrupt:
            print("KeyboardInterrupt detected, exiting process_results...")
            break
        except Exception as e:
            print(f"Error in process_results: {e}")

class SSDMobileNetV2:
    def __init__(self, model_path, ext_delegate=None, profile=True):
        self.model = model_path
        self.labels = {
            1: 'pedestrian',
            2: 'car',
            3: 'pothole',
            4: 'greenlight',
            5: 'redlight'
        }
        self.label_names = {
            1: 'pedestrian',
            2: 'car',
            3: 'pothole',
            4: 'greenlight',
            5: 'redlight'
        }
        self.CLASS_ID_MAP = {
            64: 2,
            128: 1,
            192: 3,
            256: 4,
            320: 5,
        }
        
        self.ext_delegate = None
        self.ext_delegate_options = {}
        self.profile = profile

        print(f"[SSD] initialize model: {model_path}")
        if ext_delegate is not None:
            print('[SSD] Loading external delegate from {} with args: {}'.format(
                ext_delegate, self.ext_delegate_options))
            self.ext_delegate = [
                tflite.load_delegate(ext_delegate, self.ext_delegate_options)
            ]

        # print("[SSD] create interpreter...")
        self.interpreter = tflite.Interpreter(
            model_path=self.model,
            experimental_delegates=self.ext_delegate
        )
        # print("[SSD] allocate_tensors...")
        self.interpreter.allocate_tensors()

        self.input_details = self.interpreter.get_input_details()
        self.output_details = self.interpreter.get_output_details()

        # print(f"[SSD] input_details: {self.input_details[0]}")
        # print(f"[SSD] output_details:")
        # for i, detail in enumerate(self.output_details):
        #     print(f"  输出 {i}: {detail}")

        self.input_height = self.input_details[0]['shape'][1]
        self.input_width = self.input_details[0]['shape'][2]
        self.input_type = self.input_details[0]['dtype']
        self.input_scale, self.input_zero_point = self.input_details[0]['quantization']
        # print(f"[SSD] initialize end")

    def preprocess(self, image):
        # print(f"[SSD] pre-process: {image.shape}")
        resized = cv2.resize(image, (self.input_width, self.input_height))
        rgb = cv2.cvtColor(resized, cv2.COLOR_BGR2RGB)

        if self.input_type == np.int8:
            input_data = rgb.astype(np.float32) / 255.0
            input_data = input_data / self.input_scale + self.input_zero_point
            input_data = np.clip(input_data, -128, 127).astype(np.int8)
        else:
            input_data = rgb.astype(np.float32) / 255.0

        result = np.expand_dims(input_data, axis=0)
        # print(f"[SSD] preprocess ends, result.shape=: {result.shape}")
        return result

    def inference(self, input_image):
        # print(f"[SSD] Start inference,input image shape: {input_image.shape}")
        raw_frame_shape = input_image.shape
        self.raw_frame_width = raw_frame_shape[1]
        self.raw_frame_height = raw_frame_shape[0]
        # print(f"[SSD] raw_frame_size: {self.raw_frame_width} x {self.raw_frame_height}")

        input_data = self.preprocess(input_image)

        self.interpreter.set_tensor(self.input_details[0]['index'], input_data)

        # print("[SSD] start inference...")
        if self.profile:
            time_start = time.time()
            self.interpreter.invoke()
            time_end = time.time()
            self.fps = int(1 / (time_end - time_start))
            self.inference_time = (time_end - time_start) * 1000
            # print(f"[SSD] inference time: {self.inference_time:.2f}ms, FPS: {self.fps}")
        else:
            self.interpreter.invoke()
            # print("[SSD] invoke ends")

        # print("[SSD] get_tensors...")
        scores = self.interpreter.get_tensor(self.output_details[0]['index'])
        boxes = self.interpreter.get_tensor(self.output_details[1]['index'])
        count = self.interpreter.get_tensor(self.output_details[2]['index'])
        classes = self.interpreter.get_tensor(self.output_details[3]['index'])

        # dequantization
        if scores.dtype == np.int8:
            # print("[SSD] dequantization scores...")
            scale, zero_point = self.output_details[0]['quantization']
            scores = (scores.astype(np.float32) - zero_point) * scale

        if boxes.dtype == np.int8:
            # print("[SSD] dequantization boxes...")
            scale, zero_point = self.output_details[1]['quantization']
            boxes = (boxes.astype(np.float32) - zero_point) * scale

        if classes.dtype == np.int8:
            # print("[SSD] dequantization classes...")
            scale, zero_point = self.output_details[3]['quantization']
            classes = (classes.astype(np.float32) - zero_point) * scale

        # squeeze
        scores = np.squeeze(scores)
        boxes = np.squeeze(boxes)
        classes = np.squeeze(classes).astype(int)
        count = int(np.squeeze(count))
        
        # print(f"[SSD]  scores: {scores.shape}, boxes: {boxes.shape}, count: {count}, classes: {classes.shape}")
        # print(f"[SSD] count: {count}")
        # print(f"[SSD] scores 值: {scores}")
        # print(f"[SSD] classes 值: {classes}")

        results = []
        valid_detections = 0
        for i in range(min(count, len(scores))):
            score = float(scores[i])
            # print(f"[SSD] detection {i}: score={score:.4f}, class={classes[i]}")
            if score > 0.3:
                box = boxes[i]
                class_id = int(classes[i])
                mapped_class_id = self.CLASS_ID_MAP.get(class_id, class_id)
                # print(f"[SSD] valid_detections {valid_detections}")
                
                ymin, xmin, ymax, xmax = box
                left = int(xmin * self.raw_frame_width)
                right = int(xmax * self.raw_frame_width)
                top = int(ymin * self.raw_frame_height)
                bottom = int(ymax * self.raw_frame_height)
                
                results.append([left, top, right, bottom, score, mapped_class_id])
                valid_detections += 1

        # print(f"[SSD] len(results): {len(results)}")
        # for i, result in enumerate(results):
        #     print(f"[SSD] result {i}: {result}")

        if self.profile:
            return self.inference_time, self.fps, results
        else:
            return results
        
class MobilenetV3:
    def __init__(self, model_path, ext_delegate, profile=False):
        self.model = model_path
        self.labels = ['helmet', 'nohelmet']
        self.ext_delegate = None
        self.ext_delegate_options = {}
        self.profile = profile

        # load external delegate
        if ext_delegate is not None:
            print('MobilenetV3 Loading external delegate from {} with args: {}'.format(
                    ext_delegate, self.ext_delegate_options))
            self.ext_delegate = [
                tflite.load_delegate(ext_delegate, self.ext_delegate_options)
            ]        
        
        #  initialize the interpreter
        self.interpreter = tflite.Interpreter(
            model_path = self.model,
            experimental_delegates = self.ext_delegate
        )
        self.interpreter.allocate_tensors()

        # input/output details
        self.input_details = self.interpreter.get_input_details()
        self.input_height = self.input_details[0]['shape'][1]
        self.input_width = self.input_details[0]['shape'][2]
        self.input_type = self.input_details[0]['dtype']
        self.output_details = self.interpreter.get_output_details()

    def inference(self, input_image):
        raw_frame_shape = input_image.shape
        self.raw_frame_width = raw_frame_shape[1]
        self.raw_frame_height = raw_frame_shape[0]

        # preprocess
        resized_img = cv2.resize(input_image, [self.input_height, self.input_width], interpolation=cv2.INTER_LINEAR)
        resized_img_RGB = cv2.cvtColor(resized_img, cv2.COLOR_BGR2RGB)
        resized_img_RGB = resized_img
        resized_img_RGB = np.ascontiguousarray(resized_img_RGB)

        if self.input_type == np.float32:
            input_data = np.float32(resized_img_RGB) / 255
        elif self.input_type == np.int8:
            input_data = resized_img_RGB.astype(np.int8) - 127
        else:  # uint8
            input_data = resized_img_RGB

        # seed data
        self.interpreter.set_tensor(self.input_details[0]['index'],
                                    np.expand_dims(input_data, axis=0))
        # inference

        if self.profile:
            time_start = time.time()
            self.interpreter.invoke()
            time_end = time.time()
            self.fps = (int) (1 / (time_end - time_start))
            self.inference_time = (time_end - time_start) * 1000
            # print('invoking time:')
            # print((time_end - time_start) * 1000, " ms")
        else:
            self.interpreter.invoke()

        output = self.interpreter.get_tensor(self.output_details[0]['index'])
        # print("Output shape:", output.shape)
        # print("Output data:", output)

        predicted_idx = np.argmax(output)
        confidence = output[0] * 100
        print("Predicted class index:", self.labels[predicted_idx])
        print(f"Confidence: {confidence[predicted_idx]:.2f}%")
        self.result = predicted_idx

        if self.profile:
            return self.inference_time, self.fps, self.result
        else:
            return self.result

class DemoPipeline:
    def __init__(self):
        Gst.init(None)
        self.result_queue = Queue()  
        self.mq_name = "/dms_queue"  
        self.window = Demo(self.result_queue, self.mq_name)
        self.result_process = Process(target=process_results, args=(self.result_queue, self.mq_name))
        self.result_process.start()

    def start_and_wait(self):
        try:
            GLib.MainLoop().run()
        except Exception as e:
            print(f"Error!: {e}")
            GLib.MainLoop().quit()
            self.result_queue.put(None)
            self.result_queue.join()
            self.result_process.join()


if __name__ == "__main__":
    demo = DemoPipeline()
    demo.start_and_wait()