diff --git a/packages/app/.vscode/settings.json b/packages/app/.vscode/settings.json index 8756941..fa50078 100644 --- a/packages/app/.vscode/settings.json +++ b/packages/app/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "cropperjs", "Deque", "fullscreen", "imgcodecs", diff --git a/packages/app/package.json b/packages/app/package.json index 5b5dbb1..cd522a4 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,36 +1,38 @@ -{ - "name": "omr-img-corrector", - "private": true, - "version": "0.9.1", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview", - "tauri": "tauri", - "iss": "./setup/Compiler/Compiler.exe /cc ./setup/setup.iss", - "preiss": "git submodule update --remote", - "dist": "tauri build&&yarn iss" - }, - "dependencies": { - "@emotion/react": "^11.10.6", - "@emotion/styled": "^11.10.6", - "@mui/material": "^5.11.16", - "@tauri-apps/api": "^1.2.0", - "classnames": "^2.3.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.8.1" - }, - "devDependencies": { - "@tauri-apps/cli": "^1.2.2", - "@types/node": "^18.7.10", - "@types/react": "^18.0.15", - "@types/react-dom": "^18.0.6", - "@vitejs/plugin-react": "^3.0.0", - "@vitejs/plugin-react-swc": "^3.2.0", - "less": "^4.1.3", - "typescript": "^4.6.4", - "vite": "^4.0.0" - } -} +{ + "name": "omr-img-corrector", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "tauri": "tauri", + "iss": "node ./setup/setup.cjs", + "preiss": "git submodule update --init --remote --recursive", + "dist": "tauri build&&pnpm iss" + }, + "dependencies": { + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/material": "^5.11.16", + "@tauri-apps/api": "^1.2.0", + "classnames": "^2.3.2", + "cropperjs": "^1.5.13", + "react": "^18.2.0", + "react-cropper": "^2.3.3", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^1.2.2", + "@types/node": "^18.7.10", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@vitejs/plugin-react": "^3.0.0", + "@vitejs/plugin-react-swc": "^3.2.0", + "less": "^4.1.3", + "typescript": "^4.6.4", + "vite": "^4.0.0" + } +} diff --git a/packages/app/pnpm-lock.yaml b/packages/app/pnpm-lock.yaml index 61ee41b..c3608d5 100644 --- a/packages/app/pnpm-lock.yaml +++ b/packages/app/pnpm-lock.yaml @@ -16,9 +16,15 @@ dependencies: classnames: specifier: ^2.3.2 version: 2.3.2 + cropperjs: + specifier: ^1.5.13 + version: 1.5.13 react: specifier: ^18.2.0 version: 18.2.0 + react-cropper: + specifier: ^2.3.3 + version: 2.3.3(react@18.2.0) react-dom: specifier: ^18.2.0 version: 18.2.0(react@18.2.0) @@ -1161,6 +1167,10 @@ packages: yaml: 1.10.2 dev: false + /cropperjs@1.5.13: + resolution: {integrity: sha512-by7jKAo73y5/Do0K6sxdTKHgndY0NMjG2bEdgeJxycbcmHuCiMXqw8sxy5C5Y5WTOTcDGmbT7Sr5CgKOXR06OA==} + dev: false + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} @@ -1523,6 +1533,15 @@ packages: dev: true optional: true + /react-cropper@2.3.3(react@18.2.0): + resolution: {integrity: sha512-zghiEYkUb41kqtu+2jpX2Ntigf+Jj1dF9ew4lAobPzI2adaPE31z0p+5TcWngK6TvmWQUwK3lj4G+NDh1PDQ1w==} + peerDependencies: + react: '>=17.0.2' + dependencies: + cropperjs: 1.5.13 + react: 18.2.0 + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: diff --git a/packages/app/setup/setup.cjs b/packages/app/setup/setup.cjs index b5a896e..46a461c 100644 --- a/packages/app/setup/setup.cjs +++ b/packages/app/setup/setup.cjs @@ -3,7 +3,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { version } = require(path.resolve(__dirname, '..', 'package.json')); -fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_version=${version}`); +if (!!process.env.GITHUB_OUTPUT) + fs.appendFileSync(process.env.GITHUB_OUTPUT, `release_version=${version}`); const compilerPath = path.resolve(__dirname, 'Compiler/ISCC.exe'); const setupPath = path.resolve(__dirname, 'setup.iss'); diff --git a/packages/app/src-tauri/Cargo.toml b/packages/app/src-tauri/Cargo.toml index 27ea69e..0a8e087 100644 --- a/packages/app/src-tauri/Cargo.toml +++ b/packages/app/src-tauri/Cargo.toml @@ -16,7 +16,7 @@ tauri-build = { version = "1.2", features = [] } omr-img-corrector-sdk = { version="0.9.0", path = "../../lib" } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } -tauri = { version = "1.2", features = ["dialog-confirm", "dialog-open", "fs-all", "path-all", "process-relaunch", "protocol-all", "shell-open", "window-all"] } +tauri = { version = "1.2", features = ["dialog-confirm", "dialog-open", "fs-all", "path-all", "process-relaunch", "protocol-all", "shell-execute", "shell-open", "window-all"] } sysinfo = "0.27.7" walkdir = "2.3.3" rand = "0.8.5" diff --git a/packages/app/src-tauri/src/task.rs b/packages/app/src-tauri/src/task.rs index cf58357..9bbfd42 100644 --- a/packages/app/src-tauri/src/task.rs +++ b/packages/app/src-tauri/src/task.rs @@ -11,6 +11,7 @@ struct StartRunningTaskEventPayload { #[derive(Serialize, Clone)] struct TaskCompletedEventPayload { task_id: usize, + output_path: String, result: String, } @@ -50,10 +51,12 @@ pub fn add_task( } else { "finished" }), + output_path: output_file, }, Err(_) => TaskCompletedEventPayload { task_id, result: String::from("error"), + output_path: output_file, }, }; window diff --git a/packages/app/src-tauri/tauri.conf.json b/packages/app/src-tauri/tauri.conf.json index 7e28e7f..7bd77ec 100644 --- a/packages/app/src-tauri/tauri.conf.json +++ b/packages/app/src-tauri/tauri.conf.json @@ -41,7 +41,15 @@ }, "shell": { "all": false, - "open": true + "execute": true, + "open": true, + "scope": [ + { + "name": "windows-explorer", + "cmd": "explorer", + "args": [{ "validator": ".*" }] + } + ] }, "window": { "all": true diff --git a/packages/app/src/views/Main/Task/index.tsx b/packages/app/src/views/Main/Task/index.tsx index e4682e7..cd46d6f 100644 --- a/packages/app/src/views/Main/Task/index.tsx +++ b/packages/app/src/views/Main/Task/index.tsx @@ -1,10 +1,11 @@ -import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { convertFileSrc } from '@tauri-apps/api/tauri'; import * as event from '@tauri-apps/api/event'; import styles from './index.module.less'; -import { Button, Chip, CircularProgress, Divider, LinearProgress } from '@mui/material'; +import { Chip, CircularProgress, Divider, LinearProgress } from '@mui/material'; import { Invokers } from '@/utils'; import { CaretRightIcon, CheckIcon, CloseIcon, DeleteIcon, ExclamationIcon } from '@/components'; +import openModifyWindow from '../openModifyWindow'; export interface ITaskProps { id: number; @@ -30,11 +31,16 @@ interface ITaskComponentProps extends ITaskProps { } export default forwardRef((props, ref) => { - const { id, src, omrConfig, onDelete } = props; + const { id, src, onDelete } = props; const previewSrc = useMemo(() => convertFileSrc(src), [src]); const [status, setStatus] = useState('ready'); + const [outputPath, setOutputPath] = useState(''); + const onDebate = useCallback(() => { + openModifyWindow(id, outputPath); + }, [id, outputPath]); + useEffect(() => { const unListenOnTaskRunning = event.listen('start_running_task', (ev) => { // console.log(ev); @@ -51,14 +57,15 @@ export default forwardRef((props, ref) => { const unListenOnTaskCompleted = event.listen('task_completed', (ev) => { // console.log(ev); if (ev.windowLabel !== 'main') return; - const { task_id, result } = ev.payload as { + const { task_id, result, output_path } = ev.payload as { task_id: number; result: 'finished' | 'debatable' | 'error'; + output_path: string; }; if (task_id !== id) return; setStatus((currentStatus) => { if (currentStatus !== 'running') return currentStatus; - + setOutputPath(output_path); return result; }); }); @@ -70,18 +77,14 @@ export default forwardRef((props, ref) => { }; }, [id]); - const runTask = useMemo(() => { - if (status !== 'ready') { - return () => { - // TODO: - }; - } else { - return () => { - setStatus('waiting'); - Invokers.addTask(props); - }; - } - }, [status, props]); + const runTask = useCallback(() => { + setStatus((oldStatus) => { + if (oldStatus !== 'ready') return oldStatus; + + Invokers.addTask(props); + return 'waiting'; + }); + }, [props]); useImperativeHandle( ref, () => ({ @@ -187,13 +190,15 @@ export default forwardRef((props, ref) => {
-
+
diff --git a/packages/app/src/views/Main/openModifyWindow.ts b/packages/app/src/views/Main/openModifyWindow.ts new file mode 100644 index 0000000..46f1047 --- /dev/null +++ b/packages/app/src/views/Main/openModifyWindow.ts @@ -0,0 +1,13 @@ +import { WebviewWindow } from '@tauri-apps/api/window'; + +export default async function (taskId: number, src: string) { + const webview = new WebviewWindow(`modify-${taskId}`, { + title: `人工审查 - ${src}`, + url: `views/modify.html?src=${encodeURIComponent(src)}`, + width: 600, + height: 650, + center: true, + resizable: false, + }); + webview.setFocus(); +} diff --git a/packages/app/src/views/Modify/App.module.less b/packages/app/src/views/Modify/App.module.less new file mode 100644 index 0000000..024124e --- /dev/null +++ b/packages/app/src/views/Modify/App.module.less @@ -0,0 +1,34 @@ +.app { + width: 100vw; + height: 100vh; + overflow: hidden; + display: flex; + flex-direction: column; + + .editorWrapper { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + } + + .panel { + width: 100%; + height: 15rem; + padding: 0.15rem 1.5rem; + box-sizing: border-box; + display: flex; + flex-direction: column; + justify-content: center; + + .rotate { + display: flex; + } + + .buttons { + width: 100%; + display: flex; + justify-content: space-evenly; + } + } +} diff --git a/packages/app/src/views/Modify/App.tsx b/packages/app/src/views/Modify/App.tsx new file mode 100644 index 0000000..d5543d3 --- /dev/null +++ b/packages/app/src/views/Modify/App.tsx @@ -0,0 +1,132 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import useMount from '@/hooks/useMount'; +import { writeBinaryFile } from '@tauri-apps/api/fs'; +import { convertFileSrc } from '@tauri-apps/api/tauri'; +import { getCurrent } from '@tauri-apps/api/window'; +import Cropper, { ReactCropperElement } from 'react-cropper'; +import { Button, Slider, Snackbar } from '@mui/material'; +import MuiAlert, { AlertProps } from '@mui/material/Alert'; +import styles from './App.module.less'; +import 'cropperjs/dist/cropper.css'; + +const Alert = React.forwardRef(function Alert(props, ref) { + return ; +}); + +export default function App() { + const [imageUrl, setImageUrl] = useState(''); + + useMount(() => { + let { search } = location; + if (search.startsWith('?')) search = search.slice(1); + const query = new Map(); + search.split('&').map((queryPair) => { + const [key, value] = queryPair.split('=').map(decodeURIComponent); + query.set(key, value); + }); + const src = query.get('src')!; + setImageUrl(src); + + const currentWindow = getCurrent(); + currentWindow.setFocus(); + }); + + const [rotate, setRotate] = useState(0.0); + const editorRef = useRef(null); + useEffect(() => { + editorRef.current?.cropper.rotateTo(rotate); + }, [rotate]); + + const [cropCallback, setCropCallback] = React.useState<-1 | 0 | 1>(0); + const handleClose = useCallback((event?: React.SyntheticEvent | Event, reason?: string) => { + // cSpell: disable-next-line + if (reason === 'clickaway') { + return; + } + + setCropCallback(0); + }, []); + + return ( + <> + + + {cropCallback === -1 ? '裁剪失败!' : '输出成功!'} + + +
+
+ {!!imageUrl && ( + + )} +
+
+
+ { + setRotate(value as number); + }} + /> +
+
+ + +
+
+
+ + ); +} diff --git a/packages/app/src/views/Modify/index.tsx b/packages/app/src/views/Modify/index.tsx new file mode 100644 index 0000000..461e8f9 --- /dev/null +++ b/packages/app/src/views/Modify/index.tsx @@ -0,0 +1,5 @@ +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './style.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/packages/app/src/views/Modify/style.css b/packages/app/src/views/Modify/style.css new file mode 100644 index 0000000..dac138c --- /dev/null +++ b/packages/app/src/views/Modify/style.css @@ -0,0 +1,19 @@ +:root { + font-family: Inter, Avenir, Helvetica, Arial, sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: #0f0f0f; + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; +} + +* { + margin: 0%; + padding: 0%; + outline: none; + user-select: none; +} diff --git a/packages/app/src/views/Options/Items/OutDir/index.module.less b/packages/app/src/views/Options/Items/OutDir/index.module.less index 9683696..3ab6e2f 100644 --- a/packages/app/src/views/Options/Items/OutDir/index.module.less +++ b/packages/app/src/views/Options/Items/OutDir/index.module.less @@ -10,4 +10,11 @@ display: none; } } + + &Buttons { + width: 100%; + margin-top: 1rem; + display: flex; + justify-content: space-evenly; + } } diff --git a/packages/app/src/views/Options/Items/OutDir/index.tsx b/packages/app/src/views/Options/Items/OutDir/index.tsx index 6108a98..5dbda83 100644 --- a/packages/app/src/views/Options/Items/OutDir/index.tsx +++ b/packages/app/src/views/Options/Items/OutDir/index.tsx @@ -3,7 +3,8 @@ import useLocalStorage from '@/hooks/useLocalStorage'; import useMount from '@/hooks/useMount'; import { Paths } from '@/utils'; import { Button, Input } from '@mui/material'; -import { dialog } from '@tauri-apps/api'; +import * as dialog from '@tauri-apps/api/dialog'; +import * as shell from '@tauri-apps/api/shell'; import { useCallback } from 'react'; import styles from './index.module.less'; @@ -29,6 +30,10 @@ const OutDir = () => { setOutputDir(selected as string); }, []); + const openOutputDir = useCallback(() => { + new shell.Command('windows-explorer', [outputDir]).spawn(); + }, [outputDir]); + return (
@@ -38,8 +43,13 @@ const OutDir = () => { value={outputDir} fullWidth /> +
+
+
diff --git a/packages/app/views/modify.html b/packages/app/views/modify.html new file mode 100644 index 0000000..611144c --- /dev/null +++ b/packages/app/views/modify.html @@ -0,0 +1,14 @@ + + + + + + + Tauri + React + TS + + + +
+ + + diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index 4f6cb85..147f969 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -59,6 +59,7 @@ export default defineConfig((env) => ({ rollupOptions: { input: { main: path.resolve(__dirname, 'views', 'main.html'), + modify: path.resolve(__dirname, 'views', 'modify.html'), options: path.resolve(__dirname, 'views', 'options.html'), splash: path.resolve(__dirname, 'views', 'splash.html'), test: path.resolve(__dirname, 'views', 'test.html'), diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 64ba4f5..6d338aa 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] omr-img-corrector-sdk = { version="0.9.0", path = "../lib" } rand = "0.8.5" +rand_distr = "0.4.3" walkdir = "2.3.3" [profile.dev] diff --git a/packages/core/src/main.rs b/packages/core/src/main.rs index df33b07..103e32d 100644 --- a/packages/core/src/main.rs +++ b/packages/core/src/main.rs @@ -1,13 +1,18 @@ +use noise::add_gaussian_noise; use oics::{ self, core::{self, Scalar}, - imgcodecs, imgproc, transfer, + imgcodecs, imgproc, + transfer::{self, TransformableMatrix}, types::{ImageFormat, RotateClipStrategy}, }; use rand::Rng; use std::{path::Path, time::Instant}; +mod noise; + const DATA_SET_DIR_PATH: &str = "../../dataset/dataset"; +#[allow(dead_code)] fn run_test(p: bool, h: bool, f: bool) { let instant = Instant::now(); let mut random = rand::thread_rng(); @@ -43,6 +48,20 @@ fn run_test(p: bool, h: bool, f: bool) { ) .unwrap(); + // 添加高斯噪声 + let original_image = transfer::transfer_rgb_image_to_gray_image(&original_image).unwrap(); + let original_image = TransformableMatrix::from_matrix(&{ + let mut dst = oics::prelude::Mat::default(); + imgproc::cvt_color( + &add_gaussian_noise(original_image.get_mat(), 0.0, 20.0), + &mut dst, + imgproc::COLOR_GRAY2RGB, + 0, + ) + .unwrap(); + dst + }); + if p { let projection_start = instant.elapsed().as_millis(); let projection_angle = @@ -228,7 +247,7 @@ fn run_test(p: bool, h: bool, f: bool) { } fn main() { - run_test(false, false, true); + run_test(true, true, false); } // 测试 @@ -237,13 +256,18 @@ mod tests { use oics::{ self, core::{self, Scalar}, - imgcodecs, imgproc, omr, transfer, + imgcodecs, imgproc, omr, + transfer::{self, TransformableMatrix}, types::{ImageFormat, RotateClipStrategy}, }; use rand::Rng; use std::path::Path; - use crate::DATA_SET_DIR_PATH; + #[allow(unused_imports)] + use crate::{ + noise::{add_gaussian_noise, add_salt_and_pepper_noise}, + DATA_SET_DIR_PATH, + }; #[test] fn lib_omr() { @@ -272,6 +296,24 @@ mod tests { RotateClipStrategy::DEFAULT, ) .unwrap(); + + let original_image = + transfer::transfer_rgb_image_to_gray_image(&original_image).unwrap(); + let original_image = TransformableMatrix::from_matrix(&{ + let mut dst = oics::prelude::Mat::default(); + imgproc::cvt_color( + // 添加高斯噪声 + &add_gaussian_noise(original_image.get_mat(), 0.0, 255.0), + // 添加椒盐噪声 + // &add_salt_and_pepper_noise(original_image.get_mat(), 0.01), + &mut dst, + imgproc::COLOR_GRAY2RGB, + 0, + ) + .unwrap(); + dst + }); + original_image .im_write("./tmp.jpg", ImageFormat::JPEG, 100) .unwrap(); diff --git a/packages/core/src/noise.rs b/packages/core/src/noise.rs new file mode 100644 index 0000000..13da21d --- /dev/null +++ b/packages/core/src/noise.rs @@ -0,0 +1,50 @@ +use oics::{ + core::{Mat, MatTraitConst}, + prelude::MatTraitManual, +}; + +use rand::Rng; +use rand_distr::{Distribution, Normal}; + +#[allow(dead_code)] +pub fn add_gaussian_noise(src: &Mat, mean: f64, std_dev: f64) -> Mat { + let mut rng = rand::thread_rng(); + let normal = Normal::new(mean, std_dev).unwrap(); + + // 将噪声向量转换为 Mat,与原始图像大小相同 + let mut noise_mat = src.clone(); + for i in 0..noise_mat.rows() { + let mut_row = noise_mat.at_row_mut::(i).unwrap(); + for j in 0..src.cols() { + // 从正态分布中生成随机噪声 + let noise_val = normal.sample(&mut rng); + // 在每个像素处添加噪声 + let pixel = src.at_2d::(i, j).unwrap(); + let noisy_pixel = pixel.saturating_add(noise_val as u8); + mut_row[j as usize] = noisy_pixel; + } + } + + return noise_mat; +} + +#[allow(dead_code)] +pub fn add_salt_and_pepper_noise(src: &Mat, prob: f64) -> Mat { + let mut rng = rand::thread_rng(); + + let prob = prob % 1.0; + + // 将噪声向量转换为 Mat,与原始图像大小相同 + let mut noise_mat = src.clone(); + for i in 0..noise_mat.rows() { + let mut_row = noise_mat.at_row_mut::(i).unwrap(); + for j in 0..src.cols() { + let random = rng.gen_range(0.0..1.0); + if random < prob { + mut_row[j as usize] = if random < 0.5 { 0 } else { 255 }; + } + } + } + + return noise_mat; +} diff --git a/packages/lib/Cargo.toml b/packages/lib/Cargo.toml index 9fc33ed..1d25820 100644 --- a/packages/lib/Cargo.toml +++ b/packages/lib/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] opencv = { version = "0.77.0", default-features = false, features = ["highgui", "imgcodecs", "imgproc"] } +rand = "0.8.5" [lib] name = "oics" diff --git a/packages/lib/src/lib.rs b/packages/lib/src/lib.rs index 1cd3b80..c546000 100644 --- a/packages/lib/src/lib.rs +++ b/packages/lib/src/lib.rs @@ -1,19 +1,14 @@ -pub use opencv::{core, highgui, imgcodecs, imgproc, prelude}; +pub use opencv::{ + core, highgui, imgcodecs, imgproc, prelude, types as opencv_types, Result as OpenCV_Result, +}; pub mod calculate; - pub mod constants; - pub mod fft; - pub mod hough; - pub mod omr; - pub mod projection; - pub mod transfer; - pub mod types; #[cfg(test)]