阿里3D项目-标注工具

try it

Pada tanggal 03 Februari 2024. Lihat %(latest_version_link).

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         阿里3D项目-标注工具
// @namespace    http://tampermonkey.net/
// @version      2024.2.4.1 Beta
// @description  try it
// @author       You
// @match        https://ads.aligenie.com/labeltools?type=BATCHWORK&*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aligenie.com
// @grant        none
// ==/UserScript==

(async function() {
    'use strict';

/**==================================

快捷键:
【Q】        ==>  俯视角左调
【E】        ==>  俯视角右调
【鼠标中键】 ==>  进入/退出三视图编辑模式
【空格】     ==>  帧数前切(选中个体时,控制在关联帧范围;三视图总览表→批量前切)
【`】        ==>  帧数后切(选中个体时,控制在关联帧范围;三视图总览表→批量后切)
【Ctrl + Y】 ==>   开启/关闭自动删除

自动生效:
- 个体属性面板自动保存

==================================**/

     function awaitLoad() {
        return new Promise((res, rej) => {
            let observer = new MutationObserver((mrs) => {
                // console.log('===')
                // console.log(mrs)
                mrs.some((mr) => {
                    [...mr.addedNodes].some((an) => {
                        let findEl = document.querySelector('.index_container__v2bdb')
                        if(an?.nodeName === 'DIV' && findEl) {
                            // console.log(an)
                            observer.disconnect();
                            res(true)
                        }
                    })
                })
            });
            observer.observe(document, { childList: true, subtree: true})
        })
    }

    if(await awaitLoad()) showMessage('工具已启用')

    let saveRecord = []
    let isDel = false

    new MutationObserver((mrs) => {
        if(mrs[0].addedNodes[0]?.nodeName !== '#text') console.log(mrs)
        mrs.some((mr) => {
            [...mr.addedNodes].some((an) => {
                //an.nodeName !== '#text' && console.log(an)
                if(an?.className?.includes('index_QuestionTabs__kk6nt')) {
                    an.style.border = '2px solid red'
                    new MutationObserver((mrs) => {
                        mrs.some((mr) => {
                            if(mr.type === 'attributes' && mr.target.className.includes('ant-radio-wrapper ant-radio-wrapper-checked') && mr.oldValue === 'ant-radio-wrapper') {
                                console.log('new select')
                                document.querySelector('.index_question__4Tm9w').querySelector('.ant-btn').click()
                                let time = Date.now()
                                // console.log('gen'+ time)
                                saveRecord.push([time, 0])
                            }
                        })
                    }).observe(an, { childList: true, subtree: true, attributes: true, attributeOldValue: true});
                    return true;
                }

                if(an?.className?.includes('ant-message-notice') && an.innerText === '只允许切换锚点类型') an.remove()
                if(isDel && an?.className?.includes('ant-modal-root') && an.querySelector('.ant-modal-body').innerText === '当前组内共有1个对象,删除后不可恢复,请谨慎操作!') an.querySelector('.ant-btn.ant-btn-primary.ant-btn-sm').click()
                if(isDel && an?.className?.includes('ant-table-row ant-table-row-level-0 common_rowBgColor__CUySE')) {
                    let tdIdx;
                    [...document.querySelector('.ant-table-thead').children[0].children].find((item, idx) => {
                        if(item.innerText == '对象数') tdIdx = idx
                    });

                    let delRow = [...document.querySelectorAll('.ant-table-row')][0];
                    console.log('del '+delRow.dataset.rowKey);
                    console.log([...delRow.querySelectorAll('span')].at(-1));
                    [...delRow.querySelectorAll('span')].at(-1).click()

                    return true
                }

            });
        })
    }).observe(document, { childList: true, subtree: true, attributes: true, attributeOldValue: true})

    // let attrMap = {
    //     '物体形态': 'body_shape',
    //     '形变体': 'soft',
    //     '刚体': 'rigid',
    //     '标注类别': 'type',
    //     '小汽车': 'Car',
    //     '货车/卡车': 'Truck',
    //     '公交车': 'Bus',
    //     '施工车': 'Construction',
    //     '三轮车': 'Tricycle',
    //     '骑二轮车的人': 'Cyclist',
    //     '行人': 'Pedestrian',
    //     '其他车辆': 'Other',
    //     '残影属性': 'blur',
    //     '分离': 'blur',
    //     '粘连': 'sticking',
    //     '无残影': 'no',
    //     ignore: 'ignore',
    //     '鬼影': 'multi-reflection',
    // }
    let attrMap = {
        soft:'形变体',
        rigid:  '刚体',

    }
    const originalXHR = window.XMLHttpRequest;
    window.XMLHttpRequest = function () {
        var xhr = new originalXHR();

        xhr.addEventListener('readystatechange', function () {
            if (xhr.responseURL === 'https://ads.aligenie.com/api/item/ItemServiceI/linkageSaveItems') {
                let saveRecordItem = null
                let findRes = saveRecord.some((item) => {
                    if(item[1] == 0) {
                        saveRecordItem = item
                        return true
                    }
                })
                if(!findRes) return
                // if(xhr.readyState === 2) {
                //      console.log('xhr', xhr);
                // }
                if(saveRecordItem && xhr.readyState === 4 && xhr.status == 200) {
                    let {retMsg, success, } = JSON.parse(xhr.responseText)
                    if(retMsg === 'success' && success === true) {
                        let res = JSON.parse(xhr.responseText)
                        let labels =JSON.parse(res.retValue.itemVO).items[0].labels
                        // console.log('itemOV',JSON.parse(res.retValue.itemVO).items)
                        // console.log('labels',labels)
                        showMessage(`修改完成(物体形态-${attrMap[labels.body_shape]})`, {type: 'success'})
                        saveRecordItem[1] = 1
                        saveRecord.some((item, idx) => {
                            if(item[0] === saveRecordItem[0]) {
                                saveRecord.splice(idx, 1)
                                console.log('删除'+ saveRecordItem[0])
                            }
                        })
                        console.log('请求', saveRecord)
                    }
                } else if(xhr.readyState === 4) {
                    console.log('请求响应失败')
                    showMessage('保存失败', {type: 'error'})

                    saveRecordItem[1] = 2
                }
                // console.log('xhr', xhr);
                // console.log('res', JSON.parse(xhr.responseText));
            }
        });
        return xhr;
    };


    window.addEventListener('keydown', (e) => {
        //console.log(e.keyCode, e)
        if(e.keyCode == 89 && e.ctrlKey) {
            isDel = !isDel
            showMessage(`${isDel ? '开启' : '关闭'}:列表自动删除`)
        }

        if(e.keyCode == 81) {
            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "r",
                keyCode: 82,
                code: "KeyR",
                bubbles: true, // 使事件冒泡
                cancelable: true // 使事件可以被取消
            }));
        }
        if(e.keyCode == 69) {
            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "t",
                keyCode: 84,
                code: "KeyT",
                bubbles: true, // 使事件冒泡
                cancelable: true // 使事件可以被取消
            }));
        }

        if(e.keyCode == 32 || e.keyCode == 192) {
            if(document.querySelector('.index_view__MhyQ9')) {
                let keydownMap = {
                    32: {
                        key: "ArrowRight",
                        keyCode: 39,
                        code: "ArrowRight",
                        bubbles: true,
                        cancelable: true,
                        ctrlKey: true,
                        shiftKey: true,
                    },
                    192: {
                        key: "ArrowLeft",
                        keyCode: 37,
                        code: "ArrowLeft",
                        bubbles: true,
                        cancelable: true,
                        ctrlKey: true,
                        shiftKey: true,
                    }
                }
                document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
            } else if(document.querySelector('.index_container__v2bdb').children.length || document.querySelector('.index_small-container__aVxtr')){
                turn(e.keyCode == 32 ? 'right' : 'left')

                function turn(direction) {
                    let visiblePages = [...document.querySelector('.index_trackItem__6QT87').querySelectorAll('.index_track-rect__AvEjU')];

                    if(document.querySelector('.index_absolute-center__GLVXE')) { //判断是否超出分页区
                        let curPageDom = document.querySelector('.index_absolute-center__GLVXE').parentElement
                        let curPage = document.querySelector('.index_absolute-center__GLVXE').innerText -0

                        visiblePages.some((curPage, idx) => {
                            if(curPage === curPageDom) {
                                let pages = direction === 'right' ? visiblePages.slice(idx+1) : visiblePages.slice(0, idx).reverse()
                                if(!pages.length) { //处理首页和末页
                                    let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                                    if((direction === 'right' && parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100) || (direction === 'left' && left !== '0%')) { //判断是否需要更新预览区
                                        updatePages(direction)
                                        turn(direction)
                                        return true
                                    }
                                }
                                pages.some((nextPage, nextIdx) => {
                                    if(nextPage.className.includes('index_relation__pabdZ') || nextPage.className.includes('index_key-relation__qe-c4')) {
                                        nextPage.click()
                                        return true
                                    }
                                    if(nextIdx == pages.length-1) {
                                        let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                                        if(parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100) { //判断是否需要更新预览区
                                            updatePages(direction)
                                            turn(direction)
                                            return true
                                        }
                                    }
                                })
                                return true
                            }

                            let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                            if(direction === 'right' && curPage === curPageDom && idx == visiblePages.length-1) { //当前页位于分页区末页
                                if(parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) == 100) return true

                                updatePages(direction)
                                turn(direction)
                                return true
                            } else if(direction === 'left' && curPage === curPageDom && idx == 0) { //当前页位于首页
                                if(left == '0%') return true //判断是否需要更新分页区

                                updatePages(direction)
                                turn(direction)
                                return true
                            }
                        })
                    } else {
                        let pages = direction === 'right' ? visiblePages : visiblePages.reverse()
                        console.log('pages', pages)
                        pages.some((curPage, idx) => {
                            if(curPage.className.includes('index_relation__pabdZ') || curPage.className.includes('index_key-relation__qe-c4')) {
                                console.log(curPage)
                                curPage.click()
                                return true
                            }

                            let {width, left} = document.querySelector('.index_scroll-line__GHJXu').children[0].style //progress bar
                            if(direction === 'right' && idx == pages.length-1 && (parseInt(/(.*)%/.exec(width)[1]) + parseInt(/(.*)%/.exec(left)[1]) !== 100)) {
                                updatePages(direction)
                                turn(direction)
                                return true

                            } else if(direction === 'left' && idx == pages.length-1 && left !== '0%') { //遍历至首页 且 需要更新分页区
                                updatePages(direction)
                                turn(direction)
                                return true

                            }
                        })
                    }

                    function updatePages(direction) {
                        let viewTurnBtn = document.querySelector('.index_scroll-bar__V5IcI').querySelectorAll('.anticon')[direction === 'left' ? 0 : 1]
                        viewTurnBtn.click()
                    }
                }
            } else if(!document.querySelector('.index_container__v2bdb').children.length && !document.querySelector('.index_small-container__aVxtr')){
                let keydownMap = {
                    32: {
                        key: "ArrowRight",
                        keyCode: 39,
                        code: "ArrowRight",
                        bubbles: true,
                        cancelable: true,
                        ctrlKey: true,
                    },
                    192: {
                        key: "ArrowLeft",
                        keyCode: 37,
                        code: "ArrowLeft",
                        bubbles: true,
                        cancelable: true,
                        ctrlKey: true,
                    }
                }
                document.body.dispatchEvent(new KeyboardEvent("keydown", keydownMap[e.keyCode]));
            }
        }
    });

    window.addEventListener('mousedown', (e) => {
        if(e.button == 1) {
            document.body.dispatchEvent(new KeyboardEvent("keydown", {
                key: "e",
                keyCode: 69, // 对应'E'键的keyCode
                code: "KeyE",
                ctrlKey: true,
                shiftKey: true,
                bubbles: true, // 使事件冒泡
                cancelable: true // 使事件可以被取消
            }))
        }
    })

    /**
     * @description 展示消息框
     * @param {string} message 展示内容
     * @param {object} [config] 配置对象
     * @param {string} [config.type='default'] 内容类型(可选值:'default'、'success'、'warning'、'error')
     * @param {number} [showTime=3000] 展示时间
     * @param {string} [direction='top]' 展示的位置(可选值:'top'、'top left'、'left'、'top right'、'right'、'bottom'、'bottom left'、'bottom right')
     * @return {void}
     */
    function showMessage(message, config) { //type = 'default', showTime = 3000, direction
        let MessageWrap = document.createElement('div')
        MessageWrap.className = 'messageWrap'
        setElStyle(MessageWrap, {
            position: 'absolute',
            zIndex: '9999'
        })

        let MessageBox = document.createElement('div')
        MessageBox.innerText = message

        let closeBtn = document.createElement('div')
        closeBtn.textContent = '×'
        closeBtn.addEventListener('click', MessageBox.remove.bind(MessageBox)) //关闭消息提示

        setElStyle(MessageBox, {
            position: 'relative',
            minWidth: '200px',
            marginTop: '5px',
            padding: '6px 50px',
            lineHeight: '25px',
            backgroundColor: 'pink',
            textAlign: 'center',
            fontSize: '16px',
            borderRadius: '5px',
            transition: 'all 1s'
        })

        setElStyle(closeBtn, {
            position: 'absolute',
            top: '-3px',
            right: '3px',
            width: '15px',
            height: '15px',
            zIndex: '999',
            fontWeight: '800',
            fontSize: '15px',
            borderRadius: '5px',
            cursor: 'pointer',
            userSelect: 'none'
        })
        //控制方向
        switch(config?.direction) {
            case 'top': setElStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
            case 'top left': setElStyle(MessageWrap, {top: '1%', left: '.5%'}); break;
            case 'left': setElStyle(MessageWrap, {top: '50%', left: '1%', transform: 'translateY(-50%)'}); break;
            case 'top right': setElStyle(MessageWrap, {top: '1%', right: '.5%', }); break;
            case 'right': setElStyle(MessageWrap, {top: '50%', right: '.5%', transform: 'translateY(-50%)'}); break;
            case 'bottom': setElStyle(MessageWrap, {bottom: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
            case 'bottom left': setElStyle(MessageWrap, {bottom: '1%'}); break;
            case 'bottom right': setElStyle(MessageWrap, {bottom: '1%', right: '.5%'}); break;
            default: setElStyle(MessageWrap, {top: '1%', left: '50%', transform: 'translateX(-50%)'}); break;
        }

        switch(config?.type) {
            case 'success': setElStyle(MessageBox, {border: '1.5px solid rgb(225, 243, 216)', backgroundColor: 'rgb(240, 249, 235)', color: 'rgb(103, 194, 58)'}); break;
            case 'warning': setElStyle(MessageBox, {border: '1.5px solid rgb(250, 236, 216)', backgroundColor: 'rgb(253, 246, 236)', color: 'rgb(230, 162, 60)'}); break;
            case 'error': setElStyle(MessageBox, {border: '1.5px solid rgb(253, 226, 226)', backgroundColor: 'rgb(254, 240, 240)', color: 'rgb(245, 108, 108)'}); break;
            default: setElStyle(MessageBox, {border: '1.5px solid rgba(202, 228, 255) ', backgroundColor: 'rgba(236, 245, 255)', color: 'rgb(64, 158, 255)'}); break;
        }

        MessageBox.appendChild(closeBtn)
        let oldMessageWrap = document.querySelector('.messageWrap')
        if(oldMessageWrap) {
            oldMessageWrap.appendChild(MessageBox)
        } else {
            MessageWrap.appendChild(MessageBox)
            document.body.appendChild(MessageWrap)
        }
        let ani = MessageBox.animate([
            {
                transform: "translate(0, -100%)" ,
                opacity: 0.3,
            },
            {
                transform: "translate(0, 18px)",
                opacity: 0.7,
                offset: 0.9,
            },
            {
                transform: "translate(0, 15px)",
                opacity: 1,
                offset: 1,
            },
        ], {
            duration: 300,
            fill: 'forwards',
            easing: 'ease-out',
        })

        //控制消失
        let timer = setTimeout(() => {
            ani.onfinish = () => {
                MessageBox.remove()
            }
            ani.reverse()
        }, (config?.showTime || 3000))

        //鼠标悬停时不清除,离开时重新计时
        MessageBox.addEventListener('mouseenter', () => clearTimeout(timer))
        MessageBox.addEventListener('mouseleave', () => {
            timer = setTimeout(() => {
                ani.reverse()
                ani.onfinish = () => {
                    MessageBox.remove()
                }
            }, (config?.showTime || 3000))
        })
    }


    /**
     * @description 修改元素的css样式
     * @param {Map} styleMap 样式表
     * @return {void}
     */
    function setElStyle(...args) {
        let dataType = /\[object (.*)\]/.exec(Object.prototype.toString.call(args[0]))[1]

        if (dataType === 'Map') {
            const styleMap = args[0]
            for (const [el, styleObj] of styleMap) {
                setStyleObj(el, styleObj)
            }
        } else if (dataType === 'HTMLDivElement') {
            const [el, styleObj] = args
            setStyleObj(el, styleObj)
        }

        function setStyleObj(el, styleObj) {
            for (let attr in styleObj) {
                if (el.style[attr] !== undefined) { //检查是否存在该CSS属性
                    el.style[attr] = styleObj[attr]
                } else {
                    //将key转为标准css属性名
                    let formatAttr = attr.replace(/[A-Z]/, (match) => `-${match.toLowerCase()}`)
                    console.error(el, `的 ${formatAttr} CSS属性设置失败!`)
                }
            }
        }
    }
/**
==日志===
2024/2/1
- 新增:个体属性面板自动保存

2024/2/3
- 新增:快捷键【Q】俯视角左调
- 新增:快捷键【E】俯视角右调
- 新增:快捷键【鼠标中键】进入/退出三视图编辑模式
- 新增:快捷键【空格】帧数前切(选中个体时,控制在关联帧范围;三视图总览表→批量前切)
- 新增:快捷键【`】帧数后切(选中个体时,控制在关联帧范围;三视图总览表→批量后切)
- 新增:快捷键【Ctrl + Y】 开启/关闭自动删除

=========
**/
})();