3
\$\begingroup\$

I am working on an object detection API implemented in Tensorflow 1.14 and OpenCV 4.1, where my task is to recognize personal protection equipment (PPE) worn by workers at various construction site gates.

We are using RTSP streams, where I am already using threading to minimize latency, but still some times the stream crashes. So I decided to restart the whole python script every n times of detection to prevent the whole thing crashing because of corrupted frames and so on, but Tensorflow is very slow with the loading of inference graph and such for the first time (for me it's ~20 seconds), which is unacceptable to wait for the workers to get inside the site at the gate. So now I am considering to just stop and then restart JUST the RTSP stream with OpenCV, which constantly feeds the inference machinery with frames for executing object detection on them.

Now I have not found any helpful threads on this topic, so that is why I am writing it here.

My code:

from threading import Thread
import cv2, time
import numpy as np
import os
import six.moves.urllib as urllib
import sys
import tarfile
from threading import Thread
import tensorflow as tf
import tensorflow.contrib.tensorrt as trt
import zipfile
from distutils.version import StrictVersion
from collections import defaultdict
from io import StringIO
from object_detection.utils import ops as utils_ops
import streamlit as st
import imageio
import shutil
from PIL import Image, ImageDraw


os.environ['TF_CPP_MIN_LOG_LEVEL']='2'
TF_CUDNN_USE_AUTOTUNE=0

if StrictVersion(tf.__version__) < StrictVersion('1.9.0'):
  raise ImportError('Please upgrade your TensorFlow installation to v1.9.* or later!')


from object_detection.utils import label_map_util

from object_detection.utils import visualization_utils as vis_util


MODEL_NAME = './object_detection/inference_Strabag_2020_01_20_100k_mefelelo'
modeln = "inference_Strabag_2020_01_20_100k_mefelelo"
PATH_TO_FROZEN_GRAPH = MODEL_NAME + '/frozen_inference_graph.pb'
PATH_TO_LABELS = './object_detection/training/labelmap.pbtxt'

savedir1 = "/home/dome/web/html/"
#savedir2 ="/var/www/html/"
image_store1 = 0
image_store2 = 0


detection_graph = tf.Graph()
with detection_graph.as_default():
    od_graph_def = tf.GraphDef()
    with tf.gfile.GFile(PATH_TO_FROZEN_GRAPH, 'rb') as fid:
        serialized_graph = fid.read()
        od_graph_def.ParseFromString(serialized_graph)
        tf.import_graph_def(od_graph_def, name='')

category_index = label_map_util.create_category_index_from_labelmap(PATH_TO_LABELS, use_display_name=True)

with detection_graph.as_default():
        with tf.Session(config=tf.ConfigProto(gpu_options=tf.GPUOptions(per_process_gpu_memory_fraction=0.35))) as sess:
        # Get handles to input and output tensors
            ops = tf.get_default_graph().get_operations()
            all_tensor_names = {output.name for op in ops for output in op.outputs}
            tensor_dict = {}
            for key in ['num_detections', 'detection_boxes', 'detection_scores','detection_classes', 'detection_masks']:
                tensor_name = key + ':0'
                if tensor_name in all_tensor_names:
                    tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(tensor_name)



global detected_counter1
global detected_counter2
global PASS_score1
global NOPASS_score1
global PASS_score2
global NOPASS_score2
global scoresumCounter1
global scoresumCounter2
global PASS_score_tmp
global NOPASS_score_tmp
global detection_index 
global detboxesAreaTMP1
global detboxesAreaTMP2
global imagesgif1
global imagesgif2   
resized_width = 426
resized_height = 240
rtsp_Url = 'not public'


frameST = st.empty()

def run_inference_for_single_image(image, graph):
    if 'detection_masks' in tensor_dict:
        # The following processing is only for single image
        detection_boxes = tf.squeeze(tensor_dict['detection_boxes'], [0])
        detection_masks = tf.squeeze(tensor_dict['detection_masks'], [0])
        # Reframe is required to translate mask from box coordinates to image coordinates and fit the image size.
        real_num_detection = tf.cast(tensor_dict['num_detections'][0], tf.int32)
        detection_boxes = tf.slice(detection_boxes, [0, 0], [real_num_detection, -1])
        detection_masks = tf.slice(detection_masks, [0, 0, 0], [real_num_detection, -1, -1])
        detection_masks_reframed = utils_ops.reframe_box_masks_to_image_masks(
            detection_masks, detection_boxes, image.shape[0], image.shape[1])
        detection_masks_reframed = tf.cast(
            tf.greater(detection_masks_reframed, 0.5), tf.uint8)
        # Follow the convention by adding back the batch dimension
        tensor_dict['detection_masks'] = tf.expand_dims(
            detection_masks_reframed, 0)
    image_tensor = tf.get_default_graph().get_tensor_by_name('image_tensor:0')

    # Run inference
    output_dict = sess.run(tensor_dict,
                            feed_dict={image_tensor: np.expand_dims(image, 0)})

    # all outputs are float32 numpy arrays, so convert types as appropriate
    output_dict['num_detections'] = int(output_dict['num_detections'][0])
    output_dict['detection_classes'] = output_dict[
        'detection_classes'][0].astype(np.uint8)
    output_dict['detection_boxes'] = output_dict['detection_boxes'][0]
    output_dict['detection_scores'] = output_dict['detection_scores'][0]
    if 'detection_masks' in output_dict:
        output_dict['detection_masks'] = output_dict['detection_masks'][0]
    return output_dict

detection_graph = tf.Graph()
with detection_graph.as_default():
    od_graph_def = tf.GraphDef()
    with tf.gfile.GFile(PATH_TO_FROZEN_GRAPH, 'rb') as fid:
        serialized_graph = fid.read()
        od_graph_def.ParseFromString(serialized_graph)
        tf.import_graph_def(od_graph_def, name='')

category_index = label_map_util.create_category_index_from_labelmap(PATH_TO_LABELS, use_display_name=True)



global_def_tmp = 0



class VideoStreamWidget(object):



    detected_counter1 = 0
    detected_counter2 = 0
    PASS_score1 = 0
    NOPASS_score1 = 0
    PASS_score2 = 0
    NOPASS_score2 = 0
    scoresumCounter1=0
    scoresumCounter2=0
    PASS_score_tmp =0
    NOPASS_score_tmp=0
    detection_index = 0
    detboxesAreaTMP1 = []
    detboxesAreaTMP2 = []
    image_store1 = []
    image_store2 = []
    imagesgif1 = []
    imagesgif2 = []
    def __init__(self, src=rtsp_Url):
        # Create a VideoCapture object
        #self.pipline_r = 'rtspsrc location='+rtsp_Url+' ! decodebin ! videoconvert ! appsink'
        self.capture = cv2.VideoCapture(src)
        self.capture.set(cv2.CAP_PROP_BUFFERSIZE, 1)
        self.capture.set(cv2.CAP_PROP_FPS, 4)
        self.capture.set(cv2.CAP_PROP_FRAME_WIDTH,resized_width)
        self.capture.set(cv2.CAP_PROP_FRAME_HEIGHT,resized_height)
        self.FPS = 1/4
        self.FPS_MS = int(self.FPS * 1000)

        # Start the thread to read frames from the video stream
        self.thread = Thread(target=self.update, args=())
        self.thread.daemon = True
        self.thread.start()
        self.counter = 0
        self.counter2=0
        self.status = False


    def update(self):
        # Read the next frame from the stream in a different thread

        while True:
            if self.capture.isOpened():
                #capture_buffer = np.empty(shape=(resized_width, resized_height, 3), dtype=np.uint8)
                (self.status, self.frame) = self.capture.read()
                self.frame = cv2.resize(self.frame,(resized_width,resized_height))
                #print(self.status)
            #time.sleep(self.FPS)


    def show_frame(self):
        # Display frames in main program

        if self.counter == 0 :
            #print(self.counter)
            global detected_counter1
            global detected_counter2
            global PASS_score1
            global NOPASS_score1
            global PASS_score2
            global NOPASS_score2
            global scoresumCounter1
            global scoresumCounter2
            global PASS_score_tmp
            global NOPASS_score_tmp
            global detection_index 
            global detboxesAreaTMP1
            global detboxesAreaTMP2
            global out1
            global out2
            global fourcc
            global image_store1
            global image_store2
            global imagesgif1
            global imagesgif2
            global end_time1
            global start_time1          
            detected_counter1 = 0
            detected_counter2 = 0
            PASS_score1 = 0
            NOPASS_score1 = 0
            PASS_score2 = 0
            NOPASS_score2 = 0
            scoresumCounter1=0
            scoresumCounter2=0
            PASS_score_tmp =0
            NOPASS_score_tmp=0
            detection_index = 0
            detboxesAreaTMP1 = []
            detboxesAreaTMP2 = []
            image_store1 = []
            image_store2 = []
            imagesgif1 = []
            imagesgif2 = []
            start_time1 = 0
            end_time1 = 0


            self.counter = 1

        if self.status:

            start_time = time.time()

            if detected_counter1 == 0:
                #out1 = cv2.VideoWriter(savedir1+'cegled-villa1-vidiTMP.mp4',fourcc, 4.0, (resized_width, resized_height))
                imagesgif1 = []

            if detected_counter2 == 0:
                #out2 = cv2.VideoWriter(savedir1+'cegled-villa2-vidiTMP.mp4',fourcc, 4.0, (resized_width, resized_height))
                imagesgif2 = []

            #frameST.image(self.frame, channels="BGR")
            image_np = cv2.resize(self.frame,(resized_width,resized_height))
            image_origi = image_np
            image_np_expanded = np.expand_dims(image_np, axis=0)
            image_origi_expanded = np.expand_dims(image_origi, axis=0)
            image_tensor = detection_graph.get_tensor_by_name('image_tensor:0')
            detection_boxes = detection_graph.get_tensor_by_name('detection_boxes:0')
            detection_scores = detection_graph.get_tensor_by_name('detection_scores:0')
            detection_classes = detection_graph.get_tensor_by_name('detection_classes:0')
            num_detections = detection_graph.get_tensor_by_name('num_detections:0')
            (detection_boxes, detection_scores, detection_classes, num_detections) = sess.run(
            [detection_boxes, detection_scores, detection_classes, num_detections],
            feed_dict={image_tensor: image_np_expanded})

            detection_boxes = np.asarray(detection_boxes)
            detection_classes = np.asarray(detection_classes)
            detection_scores = np.asarray(detection_scores)


            #print(end_time-start_time)

            ARO = 0.5
            scoreThreshold = 0.8

            scoresum1 = 0
            scoresum2 = 0

            avg_count = 8

            thresh = 0.1

            detboxesArea1 = [[0 for i in range(8)] for i in range(10)]
            detboxesArea2 = [[0 for i in range(8)] for i in range(10)]

            if detected_counter1==0:
                detboxesAreaTMP1 = [[0 for i in range(7)] for i in range(avg_count+2)]

                image_store1 = [0]*(avg_count+2)

            if detected_counter2==0:
                detboxesAreaTMP2 = [[0 for i in range(7)] for i in range(avg_count+2)]
                image_store2 = [0]*(avg_count+2)


            for k in range(10):
                if detection_scores[0][k] >scoreThreshold and resized_width*detection_boxes[0][k][1]>=resized_width/2.0- resized_width*0.1 :
                    detboxesArea1[k][0] = (resized_height*(detection_boxes[0][k][2]-detection_boxes[0][k][0])*resized_width*(detection_boxes[0][k][3]-detection_boxes[0][k][1]))
                    detboxesArea1[k][1] = k
                    detboxesArea1[k][2] = resized_width*detection_boxes[0][k][1] #xmin
                    detboxesArea1[k][3] = resized_height*detection_boxes[0][k][0] #ymin
                    detboxesArea1[k][4] = resized_width*detection_boxes[0][k][3] #xmax
                    detboxesArea1[k][5] = resized_height*detection_boxes[0][k][2] #ymax
                    detboxesArea1[k][6] = int(detection_classes[0][k])
                    detboxesArea1[k][7] = detection_scores[0][k]

                if detection_scores[0][k] >scoreThreshold and resized_width*detection_boxes[0][k][3]<resized_width/2.0+resized_width*0.1 :
                    detboxesArea2[k][0] = (resized_height*(detection_boxes[0][k][2]-detection_boxes[0][k][0])*resized_width*(detection_boxes[0][k][3]-detection_boxes[0][k][1]))
                    detboxesArea2[k][1] = k
                    detboxesArea2[k][2] = resized_width*detection_boxes[0][k][1]
                    detboxesArea2[k][3] = resized_height*detection_boxes[0][k][0]
                    detboxesArea2[k][4] = resized_width*detection_boxes[0][k][3]
                    detboxesArea2[k][5] = resized_height*detection_boxes[0][k][2]
                    detboxesArea2[k][6] = int(detection_classes[0][k])
                    detboxesArea2[k][7] = detection_scores[0][k]

            detboxesArea1 = sorted(detboxesArea1,reverse=True)
            detboxesArea2 = sorted(detboxesArea2,reverse=True)

            if detboxesArea1[0][3] > 0.05*resized_height and  detboxesArea1[0][0] >0 :
                detboxesAreaTMP1[detected_counter1][0] = detboxesArea1[0][0]
                detboxesAreaTMP1[detected_counter1][1] = detboxesArea1[0][2] #xmin
                detboxesAreaTMP1[detected_counter1][2] = detboxesArea1[0][3] #ymin
                detboxesAreaTMP1[detected_counter1][3] = detboxesArea1[0][4] #xmax
                detboxesAreaTMP1[detected_counter1][4] = detboxesArea1[0][5] #ymax
                detboxesAreaTMP1[detected_counter1][5] = detected_counter1
                image_store1[detected_counter1] = image_np

                detbox1Switch = 1

            if detboxesArea2[0][3] > 0.05*resized_height and detboxesArea2[0][0] >0:
                detboxesAreaTMP2[detected_counter2][0] = detboxesArea2[0][0]
                detboxesAreaTMP2[detected_counter2][1] = detboxesArea2[0][2]
                detboxesAreaTMP2[detected_counter2][2] = detboxesArea2[0][3]
                detboxesAreaTMP2[detected_counter2][3] = detboxesArea2[0][4]
                detboxesAreaTMP2[detected_counter2][4] = detboxesArea2[0][5]
                detboxesAreaTMP2[detected_counter2][5] = detected_counter2
                image_store2[detected_counter2] = image_origi

                detbox2Switch = 1



            if detboxesArea1[0][6] == 1 :
                    PASS_score1 += detboxesArea1[0][7]
                    scoresum1 += detboxesArea1[0][7]
                    cv2.rectangle(image_np, (int(detboxesArea1[0][2]),int(detboxesArea1[0][3])),(int(detboxesArea1[0][4]),int(detboxesArea1[0][5])), (37,200,37), 8)

            elif detboxesArea1[0][6] == 2:
                    NOPASS_score1 += detboxesArea1[0][7]
                    scoresum1 += detboxesArea1[0][7]
                    cv2.rectangle(image_np, (int(detboxesArea1[0][2]),int(detboxesArea1[0][3])),(int(detboxesArea1[0][4]),int(detboxesArea1[0][5])), (60,29,200), 8)


            if detboxesArea2[0][6] == 1 :
                    PASS_score2 += detboxesArea2[0][7]
                    scoresum2 += detboxesArea2[0][7]
                    cv2.rectangle(image_origi, (int(detboxesArea2[0][2]),int(detboxesArea2[0][3])),(int(detboxesArea2[0][4]),int(detboxesArea2[0][5])), (37,200,37), 8)

            elif detboxesArea2[0][6] == 2:
                    NOPASS_score2 += detboxesArea2[0][7]
                    scoresum2 += detboxesArea2[0][7]
                    cv2.rectangle(image_origi, (int(detboxesArea2[0][2]),int(detboxesArea2[0][3])),(int(detboxesArea2[0][4]),int(detboxesArea2[0][5])), (60,29,200), 8)


            # Expand dimensions since the model expects images to have shape: [1, None, None, 3]
            image_np_expanded = np.expand_dims(image_np, axis=0)
            image_origi_expanded = np.expand_dims(image_origi, axis=0)

            detected_counter1+=1
            detected_counter2+=1

            # Visualization of the results of a detection.
            '''
            vis_util.visualize_boxes_and_labels_on_image_array(
                image_np,
                np.squeeze(detection_boxes[0]),
                np.squeeze(detection_classes[0]).astype(np.int32),
                np.squeeze(detection_scores[0]),
                category_index,
                instance_masks=output_dict.get('detection_masks'),
                use_normalized_coordinates=True,
                line_thickness=8)
            '''
            #cv2.namedWindow('object_detection')
            #cv2.imshow('object_detection',image_np)

            if scoresum1 == 0:
                scoresumCounter1+=1

            if scoresum2 == 0:
                scoresumCounter2+=1



            if detected_counter1==avg_count and (avg_count-scoresumCounter1)>=int(avg_count/4.0):
                timeframe = time.time()
                detboxesAreaTMP1 = sorted(detboxesAreaTMP1,reverse=True)

                if (PASS_score1)/(avg_count-scoresumCounter1) >= (NOPASS_score1)/(avg_count-scoresumCounter1):
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(detboxesAreaTMP1[0][1]),int(detboxesAreaTMP1[0][2])),(int(detboxesAreaTMP1[0][3]),int(detboxesAreaTMP1[0][4])), (37,152,37), 8)
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(resized_width/2.0)+5,5),(resized_width-5,resized_height-5), (0,255,0), 8)
                    start_time1 = time.time()

                    cv2.imwrite(savedir1+"cegled-villa1.jpg", image_store1[detboxesAreaTMP1[0][5]])
                    os.system('php-cgi -f /home/dome/web/html/_iras.php passed=1 device_id=cegled-villa1')

                else:
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(detboxesAreaTMP1[0][1]),int(detboxesAreaTMP1[0][2])),(int(detboxesAreaTMP1[0][3]),int(detboxesAreaTMP1[0][4])), (60,29,182), 8)
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(resized_width/2.0)+5,5),(resized_width-5,resized_height-5), (0,0,255), 8)


                    cv2.imwrite(savedir1+"cegled-villa1.jpg", image_store1[detboxesAreaTMP1[0][5]])
                    os.system('php-cgi -f /home/dome/web/html/_iras.php passed=0 device_id=cegled-villa1')




                PASS_score1 = 0
                NOPASS_score1 = 0
                detboxesAreaTMP1 = []
                detected_counter1 = 0
                scoresumCounter1 = 0




            if detected_counter2==avg_count and (avg_count-scoresumCounter2)>=int(avg_count/4.0):
                timeframe = time.time()
                detboxesAreaTMP2 = sorted(detboxesAreaTMP2,reverse=True)
                if (PASS_score2)/(avg_count-scoresumCounter2) >= (NOPASS_score2)/(avg_count-scoresumCounter2):
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]], (int(detboxesAreaTMP2[0][1]),int(detboxesAreaTMP2[0][2])),(int(detboxesAreaTMP2[0][3]),int(detboxesAreaTMP2[0][4])), (0,50,0), 8)
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]],  (5,5),(int(resized_width/2.0)-5,resized_height-5), (0,255,0), 8)

                    cv2.imwrite(savedir1+"cegled-villa2.jpg", image_store2[detboxesAreaTMP2[0][5]])
                    os.system('php-cgi -f /home/dome/web/html/_iras.php passed=1 device_id=cegled-villa2')




                else:
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]], (int(detboxesAreaTMP2[0][1]),int(detboxesAreaTMP2[0][2])),(int(detboxesAreaTMP2[0][3]),int(detboxesAreaTMP2[0][4])), (0,0,50), 8)
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]],  (5,5),(int(resized_width/2.0)-5,resized_height-5), (0,0,255), 8)


                    cv2.imwrite(savedir1+"cegled-villa2.jpg", image_store2[detboxesAreaTMP2[0][5]])
                    os.system('php-cgi -f /home/dome/web/html/_iras.php passed=0 device_id=cegled-villa2')


                detected_counter2=0
                scoresumCounter2 = 0
                PASS_score2 = 0
                NOPASS_score2 = 0
                detboxesAreaTMP2 = []






            if (detected_counter1==avg_count and (avg_count-scoresumCounter1)<int(avg_count/4.0)) and (avg_count-scoresumCounter1)>0 :

                PASS_score1 = 0
                NOPASS_score1 = 0
                detected_counter1=0
                scoresumCounter1 = 0
                detboxesAreaTMP1 = []

                try:
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(detboxesAreaTMP1[0][1]),int(detboxesAreaTMP1[0][2])),(int(detboxesAreaTMP1[0][3]),int(detboxesAreaTMP1[0][4])), (60,29,182), 8)
                    cv2.rectangle(image_store1[detboxesAreaTMP1[0][5]], (int(resized_width/2.0)+5,5),(resized_width-5,resized_height-5), (0,0,255), 8)
                    cv2.imwrite(savedir1+"cegled-villa1.jpg", image_store1[detboxesAreaTMP1[0][5]])
                except IndexError:
                    cv2.imwrite(savedir1+"cegled-villa1.jpg", image_np)

                os.system('php-cgi -f /home/dome/web/html/_iras.php passed=0 device_id=cegled-villa1')

            if (detected_counter2==avg_count and (avg_count-scoresumCounter2)<int(avg_count/4.0)) and (avg_count-scoresumCounter2)>0:
                PASS_score2 = 0
                NOPASS_score2 = 0
                detected_counter2=0
                scoresumCounter2 = 0
                detboxesAreaTMP2 = []

                try:
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]], (int(detboxesAreaTMP2[0][1]),int(detboxesAreaTMP2[0][2])),(int(detboxesAreaTMP2[0][3]),int(detboxesAreaTMP2[0][4])), (0,0,50), 8)
                    cv2.rectangle(image_store2[detboxesAreaTMP2[0][5]],  (5,5),(int(resized_width/2.0)-5,resized_height-5), (0,0,255), 8)
                    cv2.imwrite(savedir1+"cegled-villa2.jpg", image_store2[detboxesAreaTMP2[0][5]])
                except IndexError:
                    cv2.imwrite(savedir1+"cegled-villa2.jpg", image_np)
                os.system('php-cgi -f /home/dome/web/html/_iras.php passed=0 device_id=cegled-villa2')

            if detected_counter1 == avg_count and (avg_count-scoresumCounter1)==0:
                detected_counter1 = 0
                PASS_score1 = 0
                NOPASS_score1 = 0
                detected_counter1=0
                scoresumCounter1 = 0
                detboxesAreaTMP1 = []


            if detected_counter2 == avg_count and (avg_count-scoresumCounter2)==0:
                detected_counter2 = 0   
                PASS_score2 = 0
                NOPASS_score2 = 0
                detected_counter2=0
                scoresumCounter2 = 0
                detboxesAreaTMP2 = []


            #cv2.namedWindow('object_detection')

            end_time = time.time()
            del image_origi
            del image_np


        # Press Q on keyboard to stop recording
        key = cv2.waitKey(2)
        if key == ord('q'):
            self.capture.release()
            cv2.destroyAllWindows()
            exit(1)



if __name__ == '__main__':




    stream_link = 'your stream link!'
    video_stream_widget = VideoStreamWidget(rtsp_Url)

    with detection_graph.as_default():
        with tf.Session(config=tf.ConfigProto(gpu_options=tf.GPUOptions(per_process_gpu_memory_fraction=0.6))) as sess:
        # Get handles to input and output tensors
            ops = tf.get_default_graph().get_operations()
            all_tensor_names = {output.name for op in ops for output in op.outputs}
            tensor_dict = {}
            for key in ['num_detections', 'detection_boxes', 'detection_scores','detection_classes', 'detection_masks']:
                tensor_name = key + ':0'
                if tensor_name in all_tensor_names:
                    tensor_dict[key] = tf.get_default_graph().get_tensor_by_name(tensor_name)
            resfresherCounter = 0
            imagesgif1 = []
            imagesgif2 = []

            while True:
                resfresherCounter+=1

                try:

                    video_stream_widget.show_frame()

                except cv2.error as e:
                    print(e)
                    detected_counter1 = 0
                    detected_counter2 = 0
                    continue


\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.