number-plate-study/car_project.ipynb

1056 lines
850 KiB
Plaintext
Raw Permalink Normal View History

2024-11-28 18:44:43 +01:00
{
"cells": [
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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jAzwjXClFEaOHFlQny233BIXXnghAN+YPOuss1p8TDNmzNDfn332WUybNg1XX3012rdvDwDo1KkTAGDVqlW45ZZbMGLECJx44olYvXo1/v73v2Pw4MF4+eWXsc0226BDhw6R8u677z784x//iCzr2bNn0bo89NBDAIBjjz028fdUKoWjjjoKkyZNwvPPP4+9994by5YtwyeffIILLrgAv/nNbzBu3Di8+uqruOKKK/D222/jkUceiRjjH330EY488kicfPLJGDVqFGpqanD44Ydj7ty52GeffQD4osD999+Pww8/HD169MDixYtx0003YeDAgXj33Xd16M+yZcvw3HPP4bnnnsPxxx+P7bffHv/85z9x/vnnY9GiRfjb3/6m9/vb3/4W06dPx2GHHYZx48bhpZdewpQpU7R3FwBcc801WLNmDQDgvffew2WXXYYLLrgAW221FQCgqqoqsV1yuRwOPfRQfPbZZ3j++efRpUuXom388ccf4+abby76exL19fUFAhAAXVeTJ554AkOGDMGmm26KiRMnoq6uDlOnTkX//v3x2muvFeTqWb16NR5++GEccsghellNTQ3KyspQX18fWXfGjBkYNWoUBg8ejMsvvxy1tbW48cYbsdtuu+H111+PlO26Lvbbbz/svPPO+POf/4y5c+diwoQJyOfzmDx5couOHwDq6uowaNAgfPzxxzj99NPRo0cPzJ49G6NHj8aKFSsKROMRI0Zg//33jyzr378/AD+cdNddd0VtbS3Gjh2Ldu3aYfr06TjwwAMxZ86cSFsUo7GxEatWrYLruvjqq68wdepUuK6L0047rcXHBgCvvPIKXnjhBQwfPhwbbbQRFi1ahBtvvBGDBg3Cu+++WzS0+PPPP8d+++2HLbfcEvfccw9SKX943dxriBBCCPnFowghhJAEampqFAD1xBNPqCVLlqjPP/9c3XXXXapdu3aqvLxcffHFF0oppS655BJVWVmpPvzww8j25513nnIcR3322WdF93Hqqaeqqqoq/ffjjz+uAKjHHnsssl6/fv3UwIEDC7bv37+/2mOPPfTfCxcuVABUTU2NXjZw4EDVp0+fFh/3woULC37L5/OqoaEhsuy7775TnTp1Uscff3xieRMmTFAtedwefPDBCoD67rvviq5z3333KQDqr3/9q1JKqVGjRikAavTo0Yn7fuihh/Sybt26KQDq3nvv1ctWrlypunTporbddlu9rL6+XrmuGylv4cKFKpvNqsmTJ+tlAwcOVADUxIkTI+uOHj1aAVBvvfWWUkqpN954QwFQv/3tbyPrnX322QqAevLJJwuO86mnnlIA1FNPPVXwm3muPc9TI0eOVBUVFeqll14qup5wxBFHqL59+6qNN95YjRo1qqDsOADW+XnllVf0+ttss43q2LGjWrZsmV723//+V9m2rY499li9TM7PiBEj1LBhw/TyTz/9VNm2rUaMGKEAqCVLliillFq9erVq06aNOvHEEyP1++abb1Tr1q0jy6VPnHHGGXqZ53lq6NChKpPJ6DKlfa644oqC4+7Tp0/kurvmmmsUADVz5ky9rLGxUe2yyy6qqqpKrVq1ap1lCmeeeaYCoJ599lm9bPXq1apHjx6qe/fuBX0viTvvvDNyDjp06KCee+65yDpyPZvnRymllixZogCoCRM
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABJ4AAAEwCAYAAAD2PbvTAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd8AUxf3Gn9m98jZekA6KgGADJBqjRhDBighiVxAVsICiEg0idlqU+EOjBktE40u1gcYuxWDvRokoYkFARUWKwMtb73bn98fuzM3u7b28L3pqkueTnNy7Nzs7dXfm2e98R0gpJQghhBBCCCGEEEII+YmxfukEEEIIIYQQQgghhJD/Tig8EUIIIYQQQgghhJC8QOGJEEIIIYQQQgghhOQFCk+EEEIIIYQQQgghJC9QeCKEEEIIIYQQQggheYHCEyGEEEIIIYQQQgjJCxSeCCGEEEIIIYQQQkheoPBECCGEEEIIIYQQQvIChSdCCCHkv5B0Oo3vv/8eX3755c9+7draWnz33Xf45ptvfvZrE0IIIYSQXxcUngghhJD/Ej777DOcf/75aNOmDRKJBFq1aoWDDz4YUsq8X/vdd9/FGWecgebNmyOZTKJNmzY4+eST835dQgghhBDy64bCEyGEkEhmzJgBIYT+FBQUYI899sDFF1+MdevW/dLJ+6+noqICkydPRvfu3VFUVITGjRujV69emDVrVqSQ9Oabb+LAAw/EkiVLcOWVV2LhwoVYvHgxHn/8cQgh8prWJ554AocccgiWL1+OG264AYsXL8bixYtxzz335PW6PxdCCFx88cWRv6l+8u677/7MqfrfZeXKlRgxYgS6du2K4uJiFBcXY4899sBFF12Ezz///JdO3k9CZWUl7rzzThx99NFo06YNGjVqhP322w933303HMf5pZNHCCGENIjYL50AQgghv24mTZqEjh07orq6Gq+++iruvvtuPPvss/jwww9RVFT0Syfvv5J169bhiCOOwMcff4xBgwbh4osvRnV1NR599FEMHToUzz77LObOnQvbtgF4S9uGDx+OPfbYA4sWLULjxo1/trRu2rQJ5513Hvr27Yt58+YhkUj8bNcm/5ssW7YML7/8Mk488UTssccecF0XX331FWbNmoWZM2fi5Zdfxm9/+9tfOpk/ii+++AKXXHIJjjjiCPzxj39EaWkpFi5ciFGjRuHNN9/EzJkzf+kkEkIIIfWGwhMhhJA66devH373u98BAM477zw0a9YMf/nLX/DEE09g8ODBv3Dq/jsZOnQoPv74Y/zjH//AwIED9fHRo0dj7NixuPnmm7Hffvth3LhxAICnnnoKn3zyCVasWPGzik4AUFZWhurqasyYMYOiE/lZ6N+/P0444YSs4xdeeCF22WUXTJs2DWVlZT9/wn5CWrdujWXLlqFr16762MiRI3HOOeegrKwM1113HTp37vwLppAQQgipP1xqRwghpEEcfvjhAIBVq1bpY5s3b8all16Kdu3aIZlMonPnzrjpppvguq4O88knn+Dwww9H69atkUwm0a5dO1xwwQXYtGkTAGDbtm0oLi7GH/7wh6xrfv3117BtG1OmTAkc79OnT2A5oPrMmDEjEKZbt2515ikqDvPTp08fAJ5l0fXXX4/9998fjRs3RnFxMXr16oUXXnhBx7V69ertxjds2LCcaXnzzTexcOFCDBs2LCA6KaZMmYLdd98dN910E6qqqvQ5HTt2xKOPPopOnTohkUhg1113xRVXXKHDKDp06IABAwZg0aJF2HfffVFQUIAuXbrgscceC4TbtGkTLr/8cuyzzz4oKSlBaWkp+vXrh3//+99Z6d13331x44036vrffffd8ec//zlQ/4Dn8Hzy5Mno1KkTkskkOnTogKuvvho1NTWB9NVVdh06dAiUs1nX5eXl2H///dGxY0d8++23OcMBwEUXXbTduvgxLFmyBL169UJxcTGaNGmC448/Hh9//HEgzIQJEyCEQMuWLZFKpQK/PfjggzrPGzZsCPz23HPP6bgbNWqE/v3746OPPgqEGTZsGEpKSvDFF1+gb9++KC4uRtu2bTFp0qTAUk1VPjfffHNWHrp166bbvuL777/Hueeei1atWqGgoAC/+c1vsqxv6uoDc+bM0eG++OILnHrqqWjatCmKiorw+9//Hs8888x2yzYej0ceb9myJYqKigJLS3MthdywYQOEEJgwYYI+tmbNGowaNQp77rknCgsL0axZM5x66qlYvXp14FwVp3n8o48+wk477YQBAwYgnU4DqH8fiqJ58+YB0Ulx4oknAkBWWyKEEEJ+zdDiiRBCSINYuXIlAKBZs2YAPF8kvXv3xtq1azFy5EjsuuuueP3113HVVVfh22+/xW233QbA81m0yy674LjjjkNpaSk+/PBD3HnnnVi7di2eeuoplJSU4MQTT8TDDz+Mv/zlL3oZGeBNwqWUGDJkSFZ69tprL1xzzTUAvMnkZZdd1uA8zZ49W39/5ZVXMH36dNx6661o3rw5AKBVq1YAgK1bt+K+++7D4MGDcf7556O8vBx///vf0bdvX7z99tvYd9990aJFi0B8jz32GP7xj38EjnXq1ClnWp566ikAwNlnnx35eywWwxlnnIGJEyfitddew5FHHomNGzfiiy++wNVXX42TTjoJY8aMwbvvvoupU6fiww8/xDPPPBOYjH/22Wc4/fTTccEFF2Do0KEoKyvDqaeeigULFuCoo44C4IkCjz/+OE499VR07NgR69atwz333IPevXtj+fLlaNu2LQBg48aNePXVV/Hqq6/inHPOwf77749//vOfuOqqq7B69Wr87W9/09c977zzMHPmTJxyyikYM2YM3nrrLUyZMkVbdwHAbbfdhm3btgHwJtc33ngjrr76auy9994AgJKSkshySaVSOPnkk/Hll1/itddeQ5s2bXKW8eeff45777035+9RVFdXZwlAAHRaTZ5//nn069cPu+22GyZMmICqqipMmzYNPXv2xHvvvafFM0V5eTmefvppLSoAniVZQUEBqqurA2Fnz56NoUOHom/fvrjppptQWVmJu+++G4cccgjef//9QNyO4+CYY47B73//e/zf//0fFixYgPHjxyOdTmPSpEkNyj8AVFVVoU+fPvj8889x8cUXo2PHjpg3bx6GDRuGzZs3Z4nGgwcPxrHHHhs41rNnTwDectIePXqgsrISo0ePRrNmzTBz5kwMHDgQ8+fPD5RFLmpra7F161Y4joNvvvkG06ZNg+M4uOiiixqcNwB455138Prrr2PQoEHYZZddsHr1atx9993o06cPli9fnnNp8VdffYVjjjkGe+21Fx555BHEYt7wur59qCF89913AKDvTYQQQsh/BJIQQgiJoKysTAKQzz//vFy/fr386quv5EMPPSSbNWsmCwsL5ddffy2llHLy5MmyuLhYfvrpp4Hzr7zySmnbtvzyyy9zXmPUqFGypKRE/71w4UIJQD733HOBcN27d5e9e/fOOr9nz57ysMMO03+vWrVKApBlZWX6WO/evWXXrl0bnO9Vq1Zl/ZZOp2VNTU3g2A8//CBbtWolzznnnMj4xo8fLxvyuD3hhBMkAPnDDz/kDPPYY49JAPKvf/2rlFLKoUOHSgBy2LBhkdd+6qmn9LH27dtLAPLRRx/Vx7Zs2SLbtGkj99tvP32surpaOo4TiG/VqlUymUzKSZMm6WO9e/eWAOSECRMCYYcNGyYByGXLlkkppVy6dKkEIM8777xAuMsvv1wCkEuWLMnK5wsvvCAByBdeeCHrN7OuXdeVQ4YMkUVFRfKtt97KGU5x2mmnyW7dusl27drJoUOHZsUdBsB2P++8844Ov++++8qWLVvKjRs36mP//ve/pWVZ8uyzz9bHVP0MHjxYDhgwQB9fs2aNtCxLDh48WAKQ69evl1JKWV5eLps0aSLPP//8QPq+++472bhx48Bx1SYuueQSfcx1Xdm/f3+ZSCR0nKp8pk6dmpXvrl27BvrdbbfdJgHIOXPm6GO1tbXy4IMPliUlJXLr1q3bjVNx6aWXSgDylVde0cfKy8tlx44dZYcOHbL
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABJ4AAAEvCAYAAAAEvQudAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd6AU1dnGn5nZvZ0iHSyAoKIowdhBBSsixBIbSBTUiB01iDURxCgxatRgiSUizYYYuwh+dmMsUaPGjmAXKVIEbtmd8/0x8868c3b2ci+6apLnp8vdnTlz5tSZc555zzuOMcaAEEIIIYQQQgghhJDvGffHTgAhhBBCCCGEEEII+e+EwhMhhBBCCCGEEEIIKQkUngghhBBCCCGEEEJISaDwRAghhBBCCCGEEEJKAoUnQgghhBBCCCGEEFISKDwRQgghhBBCCCGEkJJA4YkQQgghhBBCCCGElAQKT4QQQgghhBBCCCGkJFB4IoQQQv4LyeVy+Prrr/HJJ5/84Oeur6/HV199hS+++OIHPzchhBBCCPlpQeGJEEII+S/hgw8+wPHHH4/OnTujrKwMHTt2xC677AJjTMnP/corr+DII49Eu3btUF5ejs6dO+OQQw4p+XkJIYQQQshPGwpPhBBCUrntttvgOE70qaiowOabb45TTz0VixYt+rGT91/P6tWrcfHFF6NPnz6oqqpCq1atsNtuu2HatGmpQtI//vEP7LjjjnjiiSdw7rnn4rHHHsO8efNw3333wXGckqb1/vvvx6677oq3334bl1xyCebNm4d58+bhxhtvLOl5fygcx8Gpp56auk/6ySuvvPIDp+p/l/nz52P06NHo3bs3qqurUV1djc033xynnHIKPvzwwx87ed8bl156KXbeeWe0b98eFRUV2GyzzXDGGWdg8eLFP3bSCCGEkGaR+bETQAgh5KfNxIkT0b17d9TW1uK5557DDTfcgEceeQRvvfUWqqqqfuzk/VeyaNEi7LXXXnjnnXcwbNgwnHrqqaitrcXs2bMxcuRIPPLII5g5cyY8zwMQLG075phjsPnmm2Pu3Llo1arVD5bWZcuW4de//jUGDRqEWbNmoays7Ac7N/nf5M0338QzzzyDgw8+GJtvvjl838enn36KadOmYerUqXjmmWfw85///MdO5nfmn//8J/r27Ythw4ahRYsWeOedd3DzzTfj4Ycfxuuvv47q6uofO4mEEEJIk6DwRAghpFEGDx6M7bffHgDw61//Gm3btsWf/vQn3H///Rg+fPiPnLr/TkaOHIl33nkHf/vb33DAAQdE28eMGYNx48bhiiuuwLbbbotzzjkHAPDggw/ivffew7vvvvuDik4AMGXKFNTW1uK2226j6ER+EIYMGYKDDjqoYPtJJ52EjTbaCJMnT8aUKVN++IR9z8yePbtg2y677IJDDz0UDz74IIYNG/YjpIoQQghpPlxqRwghpFnsueeeAIAFCxZE25YvX44zzjgDG2+8McrLy9GzZ09cdtll8H0/CvPee+9hzz33RKdOnVBeXo6NN94YJ554IpYtWwYA+Pbbb1FdXY3TTz+94JyfffYZPM/DpEmTEtsHDhyYWA4on9tuuy0RZuutt240T2lx6M/AgQMBBJZFF154Ibbbbju0atUK1dXV2G233fDkk09GcS1cuHCd8Y0aNapoWv7xj3/gsccew6hRoxKikzBp0iRsttlmuOyyy7B27dromO7du2P27Nno0aMHysrKsMkmm+Dss8+OwgjdunXD0KFDMXfuXPTt2xcVFRXYaqutcO+99ybCLVu2DGeddRa22WYb1NTUoGXLlhg8eDD+9a9/FaS3b9++uPTSS6P632yzzfCHP/whUf9A4PD84osvRo8ePVBeXo5u3brh/PPPR11dXSJ9jZVdt27dEuWs63rVqlXYbrvt0L17d3z55ZdFwwHAKaecss66+C488cQT2G233VBdXY3WrVvjwAMPxDvvvJMIM2HCBDiOgw4dOqChoSGx74477ojyvGTJksS+Rx99NIq7RYsWGDJkCP79738nwowaNQo1NTX46KOPMGjQIFRXV6NLly6YOHFiYqmmlM8VV1xRkIett946avvC119/jeOOOw4dO3ZERUUFfvazn2Hq1KmJMI31gRkzZkThPvroIxx22GFo06YNqqqqsPPOO+Phhx9eZ9lms9nU7R06dEBVVVViaWmxpZBLliyB4ziYMGFCtO3jjz/GySefjC222AKVlZVo27YtDjvsMCxcuDBxrMSpt//73//GBhtsgKFDhyKXywFoeh9qDtL+ly9fvt5xEEIIIT80tHgihBDSLObPnw8AaNu2LQBgzZo1GDBgAD7//HOccMIJ2GSTTfD3v/8d5513Hr788ktcffXVAAKfRRtttBF+8YtfoGXLlnjrrbdw3XXX4fPPP8eDDz6ImpoaHHzwwbjrrrvwpz/9KVpGBgSTcGMMRowYUZCeXr164YILLgAQTCbPPPPMZudp+vTp0fdnn30WN910E6666iq0a9cOANCxY0cAwMqVK3HLLbdg+PDhOP7447Fq1Sr89a9/xaBBg/DSSy+hb9++aN++fSK+e++9F3/7298S23r06FE0LQ8++CAA4Oijj07dn8lkcOSRR+Kiiy7C888/j7333htLly7FRx99hPPPPx+//OUvMXbsWLzyyiu4/PLL8dZbb+Hhhx9OTMY/+OADHHHEETjxxBMxcuRITJkyBYcddhjmzJmDffbZB0AgCtx333047LDD0L17dyxatAg33ngjBgwYgLfffhtdunQBACxduhTPPfccnnvuORx77LHYbrvt8H//938477zzsHDhQvzlL3+JzvvrX/8aU6dOxaGHHoqxY8fixRdfxKRJkyLrLgC4+uqr8e233wIA3nnnHVx66aU4//zzseWWWwIAampqUsuloaEBhxxyCD755BM8//zz6Ny5c9Ey/vDDD3HzzTcX3Z9GbW1tgQAEIEqr5vHHH8fgwYOx6aabYsKECVi7di0mT56M/v3749VXX43EA2HVqlV46KGHcPDBB0fbpkyZgoqKCtTW1ibCTp8+HSNHjsSgQYNw2WWXYc2aNbjhhhuw66674rXXXkvEnc/nsd9++2HnnXfGH//4R8yZMwfjx49HLpfDxIkTm5V/AFi7di0GDhyIDz/8EKeeeiq6d++OWbNmYdSoUVi+fHmBaDx8+HDsv//+iW39+/cHECwn7devH9asWYMxY8agbdu2mDp1Kg444ADcc889ibIoRn19PVauXIl8Po8vvvgCkydPRj6fxymnnNLsvAHAyy+/jL///e8YNmwYNtpoIyxcuBA33HADBg4ciLfffrvo0uJPP/0U++23H3r16oW7774bmUwwvG5qH2oMYwyWLl2KXC6HDz74AOeeey48zysQBAkhhJCfNIYQQghJYcqUKQaAefzxx83ixYvNp59+au68807Ttm1bU1lZaT777DNjjDEXX3yxqa6uNu+//37i+HPPPdd4nmc++eSTouc4+eSTTU1NTfT7scceMwDMo48+mgjXp08fM2DAgILj+/fvb/bYY4/o94IFCwwAM2XKlGjbgAEDTO/evZud7wULFhTsy+Vypq6uLrHtm2++MR07djTHHntsanzjx483zbndHnTQQQaA+eabb4qGuffeew0A8+c//9kYY8zIkSMNADNq1KjUcz/44IPRtq5duxoAZvbs2dG2FStWmM6dO5ttt9022lZbW2vy+XwivgULFpjy8nIzceLEaNuAAQMMADNhwoRE2FGjRhkA5s033zTGGPP6668bAObXv/51ItxZZ51lAJgnnniiIJ9PPvmkAWCefPLJgn26rn3fNyNGjDBVVVXmxRdfLBpOOPzww83WW29tNt54YzNy5MiCuG0ArPPz8ssvR+H79u1rOnToYJYuXRpt+9e//mVc1zVHH310tE3qZ/jw4Wbo0KHR9o8//ti4rmuGDx9uAJjFixcbY4xZtWqVad26tTn++OMT6fvqq69Mq1atEtulTZx22mnRNt/3zZAhQ0xZWVkUp5TP5ZdfXpDv3r17J/rd1VdfbQCYGTNmRNvq6+vNLrv
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import cv2\n",
"import numpy as np\n",
"from matplotlib import pyplot as plt\n",
"from PIL import Image, ImageOps\n",
"\n",
"# 1. Функция для упорядочивания точек\n",
"def order_points(pts):\n",
" \"\"\"\n",
" Упорядочивает точки в порядке:\n",
" верхний левый, верхний правый, нижний правый, нижний левый\n",
" \"\"\"\n",
" rect = np.zeros((4, 2), dtype=\"float32\")\n",
"\n",
" s = pts.sum(axis=1)\n",
" rect[0] = pts[np.argmin(s)] \n",
" rect[2] = pts[np.argmax(s)]\n",
"\n",
" diff = np.diff(pts, axis=1)\n",
" rect[1] = pts[np.argmin(diff)] \n",
" rect[3] = pts[np.argmax(diff)] \n",
"\n",
" return rect\n",
"\n",
"# 2. Функция для перспективного преобразования\n",
"def get_transform(image, pts, padding=0.1):\n",
" \"\"\"\n",
" Применяет перспективное преобразование к области, определенной точками pts в изображении.\n",
" Уменьшает область захвата на заданный процент (padding).\n",
" Возвращает обрезанное и преобразованное изображение номерного знака.\n",
" \n",
" :param image: Исходное изображение\n",
" :param pts: Точки контура номерного знака (4 точки)\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" \"\"\"\n",
" rect = order_points(pts)\n",
" (tl, tr, br, bl) = rect\n",
"\n",
" widthA = np.linalg.norm(br - bl)\n",
" widthB = np.linalg.norm(tr - tl)\n",
" maxWidth = max(int(widthA), int(widthB))\n",
"\n",
" heightA = np.linalg.norm(tr - br)\n",
" heightB = np.linalg.norm(tl - bl)\n",
" maxHeight = max(int(heightA), int(heightB))\n",
"\n",
" center = np.mean(rect, axis=0)\n",
"\n",
" vectors = rect - center\n",
"\n",
" rect_shrinked = rect - vectors * padding\n",
"\n",
" rect_shrinked[:, 0] = np.clip(rect_shrinked[:, 0], 0, image.shape[1] - 1)\n",
" rect_shrinked[:, 1] = np.clip(rect_shrinked[:, 1], 0, image.shape[0] - 1)\n",
"\n",
" dst = np.array([\n",
" [0, 0],\n",
" [maxWidth - 1, 0],\n",
" [maxWidth - 1, maxHeight - 1],\n",
" [0, maxHeight - 1]\n",
" ], dtype=\"float32\")\n",
"\n",
" M = cv2.getPerspectiveTransform(rect_shrinked, dst)\n",
"\n",
" warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))\n",
"\n",
" return warped\n",
"\n",
"def get_transformed_plate(image, bbox, padding=0.1):\n",
" \"\"\"\n",
" Выполняет перспективное преобразование для извлечения номерного знака из изображения.\n",
" Уменьшает область захвата на заданный процент (padding).\n",
" \n",
" :param image: Исходное изображение\n",
" :param bbox: Координаты ограничивающего прямоугольника (4 точки)\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" :return: Преобразованное изображение номерного знака\n",
" \"\"\"\n",
" plate = get_transform(image, bbox, padding)\n",
" return plate\n",
"\n",
"# 4. Функция для изменения размера с сохранением соотношения сторон и добавлением отступов\n",
"def resize_with_padding(image, size=28, padding=4, bg_color=255):\n",
" \"\"\"\n",
" Изменяет размер изображения до заданного размера с сохранением соотношения сторон\n",
" и добавлением отступов.\n",
" \n",
" :param image: Градация серого изображения символа (numpy array).\n",
" :param size: Целевой размер (ширина и высота) итогового изображения.\n",
" :param padding: Количество пикселей для отступов.\n",
" :param bg_color: Цвет фона (0 для чёрного, 255 для белого).\n",
" :return: Изображение размером size x size пикселей.\n",
" \"\"\"\n",
" # Преобразуем numpy array в PIL Image\n",
" pil_img = Image.fromarray(image)\n",
" \n",
" # Определяем текущие размеры\n",
" w, h = pil_img.size\n",
" \n",
" # Рассчитываем масштабирование\n",
" scale = (size - 2 * padding) / max(w, h)\n",
" new_w = int(w * scale)\n",
" new_h = int(h * scale)\n",
" \n",
" # Изменяем размер с сохранением соотношения сторон\n",
" pil_resized = pil_img.resize((new_w, new_h), Image.LANCZOS)\n",
" \n",
" # Создаём новое изображение с фоном\n",
" new_img = Image.new(\"L\", (size, size), color=bg_color)\n",
" \n",
" # Вычисляем позицию для центрирования\n",
" paste_x = (size - new_w) // 2\n",
" paste_y = (size - new_h) // 2\n",
" \n",
" # Вставляем изменённое изображение на фон\n",
" new_img.paste(pil_resized, (paste_x, paste_y))\n",
" \n",
" # Преобразуем обратно в numpy array\n",
" return np.array(new_img)\n",
"\n",
"# 5. Обновлённые маски\n",
"mask_8 = [\n",
" (0.05, 0.25, 0.1, 0.5), # Символ 1: Буква\n",
" (0.17, 0.25, 0.1, 0.5), # Символ 2: Цифра\n",
" (0.29, 0.25, 0.1, 0.5), # Символ 3: Цифра\n",
" (0.41, 0.25, 0.1, 0.5), # Символ 4: Цифра\n",
" (0.53, 0.25, 0.1, 0.5), # Символ 5: Буква\n",
" (0.65, 0.25, 0.1, 0.5), # Символ 6: Буква\n",
" (0.80, 0.05, 0.1, 0.5), # Символ 7: Региональная цифра 1 (выше и меньше)\n",
" (0.90, 0.05, 0.1, 0.5) # Символ 8: Региональная цифра 2 (выше и меньше)\n",
"]\n",
"\n",
"mask_9 = [\n",
" (0.05, 0.25, 0.1, 0.5), # Символ 1: Буква\n",
" (0.17, 0.25, 0.1, 0.5), # Символ 2: Цифра\n",
" (0.29, 0.25, 0.1, 0.5), # Символ 3: Цифра\n",
" (0.41, 0.25, 0.1, 0.5), # Символ 4: Цифра\n",
" (0.53, 0.25, 0.1, 0.5), # Символ 5: Буква\n",
" (0.65, 0.25, 0.1, 0.5), # Символ 6: Буква\n",
" (0.77, 0.05, 0.1, 0.6), # Символ 7: Региональная цифра 1\n",
" (0.89, 0.05, 0.1, 0.6), # Символ 8: Региональная цифра 2\n",
" (1.01, 0.05, 0.1, 0.6) # Символ 9: Региональная цифра 3\n",
"]\n",
"\n",
"# 6. Функция для извлечения символов\n",
"def extract_symbols(image, masks):\n",
" \"\"\"\n",
" Извлекает символы из изображения на основе предоставленных масок.\n",
" \n",
" :param image: Градация серого изображения номерного знака.\n",
" :param masks: Список кортежей с определениями маски как (x_frac, y_frac, w_frac, h_frac).\n",
" :return: Список извлечённых и изменённых по размеру изображений символов.\n",
" \"\"\"\n",
" img_h, img_w = image.shape\n",
" symbols = []\n",
" for bbox in masks:\n",
" x_frac, y_frac, w_frac, h_frac = bbox\n",
" x = int(x_frac * img_w)\n",
" y = int(y_frac * img_h)\n",
" w = int(w_frac * img_w)\n",
" h = int(h_frac * img_h)\n",
" \n",
" # Убедимся, что область находится внутри границ изображения\n",
" x_end = min(x + w, img_w)\n",
" y_end = min(y + h, img_h)\n",
" \n",
" symbol = image[y:y_end, x:x+w]\n",
" if symbol.size == 0:\n",
" # Если область вне границ изображения, пропускаем\n",
" continue\n",
" # Изменяем размер с сохранением соотношения сторон и добавлением отступов\n",
" symbol_resized = resize_with_padding(symbol, size=28, padding=2, bg_color=255)\n",
" symbols.append(symbol_resized)\n",
" return symbols\n",
"\n",
"# 7. Функция для проверки наличия символа\n",
"def is_symbol_present(symbol, threshold=250):\n",
" \"\"\"\n",
" Определяет, присутствует ли символ на изображении на основе средней интенсивности пикселей.\n",
" \n",
" :param symbol: Градация серого изображения символа.\n",
" :param threshold: Порог интенсивности для определения присутствия символа.\n",
" :return: Логическое значение, указывающее на наличие символа.\n",
" \"\"\"\n",
" return np.mean(symbol) < threshold # Тёмные символы имеют меньшую среднюю интенсивность\n",
"\n",
"# 8. Основная функция обработки одной пластинки\n",
"def process_plate(plate):\n",
" \"\"\"\n",
" Обрабатывает одно изображение номерного знака для извлечения отдельных символов на основе масок.\n",
" \n",
" :param plate: BGR изображение вырезанного номерного знака.\n",
" :return: Кортеж, содержащий обработанное изображение и список извлечённых символов.\n",
" \"\"\"\n",
" if plate is None:\n",
" return None, []\n",
" \n",
" # Преобразование в градации серого\n",
" gray_plate = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY)\n",
"\n",
" # Увеличение размера для консистентной обработки\n",
" resized_plate = cv2.resize(gray_plate, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)\n",
" img_h, img_w = resized_plate.shape\n",
"\n",
" # Применение гауссового размытия для снижения шума\n",
" blurred = cv2.GaussianBlur(resized_plate, (3, 3), 0)\n",
"\n",
" # Извлечение символов с использованием обеих масок\n",
" symbols_8 = extract_symbols(blurred, mask_8)\n",
" symbols_9 = extract_symbols(blurred, mask_9)\n",
"\n",
" # Определение, какую маску использовать, основываясь на наличии третьей региональной цифры\n",
" if len(symbols_9) == 9 and is_symbol_present(symbols_9[-1]):\n",
" characters = symbols_9\n",
" else:\n",
" characters = symbols_8\n",
"\n",
" return blurred, characters\n",
"\n",
"# 9. Функция для визуализации маски (опционально, для калибровки)\n",
"def visualize_mask(image, masks):\n",
" \"\"\"\n",
" Отображает области маски на изображении для визуальной проверки.\n",
" \n",
" :param image: Градация серого изображения номерного знака.\n",
" :param masks: Список кортежей с определениями маски.\n",
" \"\"\"\n",
" img_h, img_w = image.shape\n",
" img_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)\n",
" for bbox in masks:\n",
" x_frac, y_frac, w_frac, h_frac = bbox\n",
" x = int(x_frac * img_w)\n",
" y = int(y_frac * img_h)\n",
" w = int(w_frac * img_w)\n",
" h = int(h_frac * img_h)\n",
" cv2.rectangle(img_color, (x, y), (x + w, y + h), (0, 255, 0), 2)\n",
" plt.imshow(cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB))\n",
" plt.axis('off')\n",
" plt.show()\n",
"\n",
"# 10. Код для сбора processed_plates\n",
"def gather_processed_plates(image_paths, padding=0.1):\n",
" \"\"\"\n",
" Загружает изображения, обнаруживает номерные знаки и извлекает их.\n",
" \n",
" :param image_paths: Список путей к изображениям.\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" :return: Список преобразованных изображений номерных знаков.\n",
" \"\"\"\n",
" processed_plates = []\n",
" \n",
" for img_path in image_paths:\n",
" image = cv2.imread(img_path)\n",
" \n",
" if image is None:\n",
" print(f\"Не удалось загрузить изображение по пути: {img_path}\")\n",
" processed_plates.append(None)\n",
" continue\n",
"\n",
" img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n",
" \n",
" ret, thresh = cv2.threshold(img_gray, 100, 200, cv2.THRESH_TOZERO_INV)\n",
" \n",
" contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n",
"\n",
" plate_found = False \n",
"\n",
" for cnt in contours:\n",
" x, y, w, h = cv2.boundingRect(cnt)\n",
" area = w * h\n",
" aspectRatio = float(w) / h\n",
"\n",
" if aspectRatio >= 3 and area > 600:\n",
" approx = cv2.approxPolyDP(cnt, 0.05 * cv2.arcLength(cnt, True), True)\n",
" \n",
" if len(approx) <= 4 and x > 15:\n",
" rect = cv2.minAreaRect(cnt)\n",
" box = cv2.boxPoints(rect)\n",
" box = np.intp(box)\n",
"\n",
" plate = get_transformed_plate(image, box, padding=padding) \n",
"\n",
" processed_plates.append(plate)\n",
" plate_found = True\n",
" break \n",
"\n",
" if not plate_found:\n",
" print(f\"Номерной знак не найден на изображении: {img_path}\")\n",
" processed_plates.append(None)\n",
" \n",
" return processed_plates\n",
"\n",
"# 11. Пример использования\n",
"if __name__ == \"__main__\":\n",
" # Пути к изображениям\n",
" images = ['img/1.jpg', 'img/2.jpg', 'img/3.jpg']\n",
"\n",
" # Сборка processed_plates\n",
" processed_plates = gather_processed_plates(images, padding=0.1)\n",
"\n",
" num_plates = len(processed_plates)\n",
"\n",
" cols = min(num_plates, 3)\n",
" rows = (num_plates + cols - 1) // cols \n",
"\n",
" fig, axs = plt.subplots(rows, cols, figsize=(5 * cols, 5 * rows))\n",
" if num_plates == 1:\n",
" axs = [axs] \n",
" else:\n",
" axs = axs.flatten()\n",
"\n",
" for i, plate in enumerate(processed_plates):\n",
" ax = axs[i]\n",
" if plate is not None:\n",
" if len(plate.shape) == 3:\n",
" plate_rgb = cv2.cvtColor(plate, cv2.COLOR_BGR2RGB)\n",
" ax.imshow(plate_rgb)\n",
" else:\n",
" ax.imshow(plate, cmap='gray')\n",
" else:\n",
" ax.text(0.5, 0.5, 'Номер не найден', horizontalalignment='center', verticalalignment='center', fontsize=12, color='red')\n",
" \n",
" ax.set_title(f'Номер {i+1}')\n",
" ax.axis('off')\n",
" \n",
" # Удаление лишних субплотов, если они есть\n",
" for j in range(i + 1, len(axs)):\n",
" fig.delaxes(axs[j])\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" # Обработка и отображение извлечённых символов\n",
" for plate_index, plate in enumerate(processed_plates):\n",
" # Проверка корректности загрузки изображения\n",
" if plate is None:\n",
" print(f\"Номерной знак {plate_index + 1} не обработан.\")\n",
" continue\n",
"\n",
" # Обработка номерного знака для извлечения символов\n",
" processed_plate, characters = process_plate(plate)\n",
"\n",
" # Отображение результатов\n",
" plt.figure(figsize=(15, 5))\n",
"\n",
" # Исходный номерной знак\n",
" plt.subplot(1, 3, 1)\n",
" plt.title(\"Исходный Номерной Знак\")\n",
" plt.imshow(cv2.cvtColor(plate, cv2.COLOR_BGR2RGB))\n",
" plt.axis('off')\n",
"\n",
" # Обработанное изображение (градации серого и размытие)\n",
" plt.subplot(1, 3, 2)\n",
" plt.title(\"Обработанный Номерной Знак\")\n",
" plt.imshow(processed_plate, cmap='gray')\n",
" plt.axis('off')\n",
"\n",
" # Извлечённые символы\n",
" plt.subplot(1, 3, 3)\n",
" plt.title(\"Извлечённые Символы\")\n",
" if characters:\n",
" # Располагаем символы в горизонтальном ряду\n",
" symbols_grid = np.hstack(characters)\n",
" plt.imshow(symbols_grid, cmap='gray')\n",
" else:\n",
" plt.text(0.5, 0.5, 'Символы не обнаружены', ha='center', va='center', fontsize=12)\n",
" plt.axis('off')\n",
"\n",
" plt.suptitle(f\"Результат Обработки Номерного Знака {plate_index + 1}\")\n",
" plt.show()\n"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png": "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
"text/plain": [
"<Figure size 1500x500 with 3 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import os\n",
"import cv2\n",
"import numpy as np\n",
"from matplotlib import pyplot as plt\n",
"from PIL import Image, ImageOps\n",
"import torch\n",
"import torch.nn as nn\n",
"from torchvision import transforms\n",
"\n",
"# 1. Функция для упорядочивания точек\n",
"def order_points(pts):\n",
" \"\"\"\n",
" Упорядочивает точки в порядке:\n",
" верхний левый, верхний правый, нижний правый, нижний левый\n",
" \"\"\"\n",
" rect = np.zeros((4, 2), dtype=\"float32\")\n",
"\n",
" s = pts.sum(axis=1)\n",
" rect[0] = pts[np.argmin(s)] \n",
" rect[2] = pts[np.argmax(s)]\n",
"\n",
" diff = np.diff(pts, axis=1)\n",
" rect[1] = pts[np.argmin(diff)] \n",
" rect[3] = pts[np.argmax(diff)] \n",
"\n",
" return rect\n",
"\n",
"# 2. Функция для перспективного преобразования\n",
"def get_transform(image, pts, padding=0.1):\n",
" \"\"\"\n",
" Применяет перспективное преобразование к области, определенной точками pts в изображении.\n",
" Уменьшает область захвата на заданный процент (padding).\n",
" Возвращает обрезанное и преобразованное изображение номерного знака.\n",
" \n",
" :param image: Исходное изображение\n",
" :param pts: Точки контура номерного знака (4 точки)\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" \"\"\"\n",
" rect = order_points(pts)\n",
" (tl, tr, br, bl) = rect\n",
"\n",
" widthA = np.linalg.norm(br - bl)\n",
" widthB = np.linalg.norm(tr - tl)\n",
" maxWidth = max(int(widthA), int(widthB))\n",
"\n",
" heightA = np.linalg.norm(tr - br)\n",
" heightB = np.linalg.norm(tl - bl)\n",
" maxHeight = max(int(heightA), int(heightB))\n",
"\n",
" center = np.mean(rect, axis=0)\n",
"\n",
" vectors = rect - center\n",
"\n",
" rect_shrinked = rect - vectors * padding\n",
"\n",
" rect_shrinked[:, 0] = np.clip(rect_shrinked[:, 0], 0, image.shape[1] - 1)\n",
" rect_shrinked[:, 1] = np.clip(rect_shrinked[:, 1], 0, image.shape[0] - 1)\n",
"\n",
" dst = np.array([\n",
" [0, 0],\n",
" [maxWidth - 1, 0],\n",
" [maxWidth - 1, maxHeight - 1],\n",
" [0, maxHeight - 1]\n",
" ], dtype=\"float32\")\n",
"\n",
" M = cv2.getPerspectiveTransform(rect_shrinked, dst)\n",
"\n",
" warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))\n",
"\n",
" return warped\n",
"\n",
"def get_transformed_plate(image, bbox, padding=0.1):\n",
" \"\"\"\n",
" Выполняет перспективное преобразование для извлечения номерного знака из изображения.\n",
" Уменьшает область захвата на заданный процент (padding).\n",
" \n",
" :param image: Исходное изображение\n",
" :param bbox: Координаты ограничивающего прямоугольника (4 точки)\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" :return: Преобразованное изображение номерного знака\n",
" \"\"\"\n",
" plate = get_transform(image, bbox, padding)\n",
" return plate\n",
"\n",
"# 4. Функция для изменения размера с сохранением соотношения сторон и добавлением отступов\n",
"def resize_with_padding(image, size=28, padding=4, bg_color=255):\n",
" \"\"\"\n",
" Изменяет размер изображения до заданного размера с сохранением соотношения сторон\n",
" и добавлением отступов.\n",
" \n",
" :param image: Градация серого изображения символа (numpy array).\n",
" :param size: Целевой размер (ширина и высота) итогового изображения.\n",
" :param padding: Количество пикселей для отступов.\n",
" :param bg_color: Цвет фона (0 для чёрного, 255 для белого).\n",
" :return: Изображение размером size x size пикселей.\n",
" \"\"\"\n",
" # Преобразуем numpy array в PIL Image\n",
" pil_img = Image.fromarray(image)\n",
" \n",
" # Определяем текущие размеры\n",
" w, h = pil_img.size\n",
" \n",
" # Рассчитываем масштабирование\n",
" scale = (size - 2 * padding) / max(w, h)\n",
" new_w = int(w * scale)\n",
" new_h = int(h * scale)\n",
" \n",
" # Изменяем размер с сохранением соотношения сторон\n",
" pil_resized = pil_img.resize((new_w, new_h), Image.LANCZOS)\n",
" \n",
" # Создаём новое изображение с фоном\n",
" new_img = Image.new(\"L\", (size, size), color=bg_color)\n",
" \n",
" # Вычисляем позицию для центрирования\n",
" paste_x = (size - new_w) // 2\n",
" paste_y = (size - new_h) // 2\n",
" \n",
" # Вставляем изменённое изображение на фон\n",
" new_img.paste(pil_resized, (paste_x, paste_y))\n",
" \n",
" # Преобразуем обратно в numpy array\n",
" return np.array(new_img)\n",
"\n",
"# 5. Обновлённые маски\n",
"mask_8 = [\n",
" (0.05, 0.25, 0.1, 0.5), # Символ 1: Буква\n",
" (0.17, 0.25, 0.1, 0.5), # Символ 2: Цифра\n",
" (0.29, 0.25, 0.1, 0.5), # Символ 3: Цифра\n",
" (0.41, 0.25, 0.1, 0.5), # Символ 4: Цифра\n",
" (0.53, 0.25, 0.1, 0.5), # Символ 5: Буква\n",
" (0.65, 0.25, 0.1, 0.5), # Символ 6: Буква\n",
" (0.80, 0.05, 0.1, 0.5), # Символ 7: Региональная цифра 1 (выше и меньше)\n",
" (0.90, 0.05, 0.1, 0.5) # Символ 8: Региональная цифра 2 (выше и меньше)\n",
"]\n",
"\n",
"mask_9 = [\n",
" (0.05, 0.25, 0.1, 0.5), # Символ 1: Буква\n",
" (0.17, 0.25, 0.1, 0.5), # Символ 2: Цифра\n",
" (0.29, 0.25, 0.1, 0.5), # Символ 3: Цифра\n",
" (0.41, 0.25, 0.1, 0.5), # Символ 4: Цифра\n",
" (0.53, 0.25, 0.1, 0.5), # Символ 5: Буква\n",
" (0.65, 0.25, 0.1, 0.5), # Символ 6: Буква\n",
" (0.77, 0.05, 0.1, 0.6), # Символ 7: Региональная цифра 1\n",
" (0.89, 0.05, 0.1, 0.6), # Символ 8: Региональная цифра 2\n",
" (1.01, 0.05, 0.1, 0.6) # Символ 9: Региональная цифра 3\n",
"]\n",
"\n",
"# 6. Функция для извлечения символов\n",
"def extract_symbols(image, masks):\n",
" \"\"\"\n",
" Извлекает символы из изображения на основе предоставленных масок.\n",
" \n",
" :param image: Градация серого изображения номерного знака.\n",
" :param masks: Список кортежей с определениями маски как (x_frac, y_frac, w_frac, h_frac).\n",
" :return: Список извлечённых и изменённых по размеру изображений символов.\n",
" \"\"\"\n",
" img_h, img_w = image.shape\n",
" symbols = []\n",
" for bbox in masks:\n",
" x_frac, y_frac, w_frac, h_frac = bbox\n",
" x = int(x_frac * img_w)\n",
" y = int(y_frac * img_h)\n",
" w = int(w_frac * img_w)\n",
" h = int(h_frac * img_h)\n",
" \n",
" # Убедимся, что область находится внутри границ изображения\n",
" x_end = min(x + w, img_w)\n",
" y_end = min(y + h, img_h)\n",
" \n",
" symbol = image[y:y_end, x:x+w]\n",
" if symbol.size == 0:\n",
" # Если область вне границ изображения, пропускаем\n",
" continue\n",
" # Изменяем размер с сохранением соотношения сторон и добавлением отступов\n",
" symbol_resized = resize_with_padding(symbol, size=28, padding=2, bg_color=255)\n",
" symbols.append(symbol_resized)\n",
" return symbols\n",
"\n",
"# 7. Функция для проверки наличия символа\n",
"def is_symbol_present(symbol, threshold=250):\n",
" \"\"\"\n",
" Определяет, присутствует ли символ на изображении на основе средней интенсивности пикселей.\n",
" \n",
" :param symbol: Градация серого изображения символа.\n",
" :param threshold: Порог интенсивности для определения присутствия символа.\n",
" :return: Логическое значение, указывающее на наличие символа.\n",
" \"\"\"\n",
" return np.mean(symbol) < threshold # Тёмные символы имеют меньшую среднюю интенсивность\n",
"\n",
"# 8. Основная функция обработки одной пластинки\n",
"def process_plate(plate):\n",
" \"\"\"\n",
" Обрабатывает одно изображение номерного знака для извлечения отдельных символов на основе масок.\n",
" \n",
" :param plate: BGR изображение вырезанного номерного знака.\n",
" :return: Кортеж, содержащий обработанное изображение, список извлечённых символов и ожидаемые типы символов.\n",
" \"\"\"\n",
" if plate is None:\n",
" return None, [], []\n",
" \n",
" # Преобразование в градации серого\n",
" gray_plate = cv2.cvtColor(plate, cv2.COLOR_BGR2GRAY)\n",
"\n",
" # Увеличение размера для консистентной обработки\n",
" resized_plate = cv2.resize(gray_plate, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)\n",
" img_h, img_w = resized_plate.shape\n",
"\n",
" # Применение гауссового размытия для снижения шума\n",
" blurred = cv2.GaussianBlur(resized_plate, (3, 3), 0)\n",
"\n",
" # Извлечение символов с использованием обеих масок\n",
" symbols_8 = extract_symbols(blurred, mask_8)\n",
" symbols_9 = extract_symbols(blurred, mask_9)\n",
"\n",
" # Определение, какую маску использовать, основываясь на наличии третьей региональной цифры\n",
" if len(symbols_9) == 9 and is_symbol_present(symbols_9[-1]):\n",
" characters = symbols_9\n",
" expected_types = ['letter', 'digit', 'digit', 'digit', 'letter', 'letter', 'digit', 'digit', 'digit']\n",
" else:\n",
" characters = symbols_8\n",
" expected_types = ['letter', 'digit', 'digit', 'digit', 'letter', 'letter', 'digit', 'digit']\n",
"\n",
" return blurred, characters, expected_types\n",
"\n",
"# 9. Функция для визуализации маски (опционально, для калибровки)\n",
"def visualize_mask(image, masks):\n",
" \"\"\"\n",
" Отображает области маски на изображении для визуальной проверки.\n",
" \n",
" :param image: Градация серого изображения номерного знака.\n",
" :param masks: Список кортежей с определениями маски.\n",
" \"\"\"\n",
" img_h, img_w = image.shape\n",
" img_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)\n",
" for bbox in masks:\n",
" x_frac, y_frac, w_frac, h_frac = bbox\n",
" x = int(x_frac * img_w)\n",
" y = int(y_frac * img_h)\n",
" w = int(w_frac * img_w)\n",
" h = int(h_frac * img_h)\n",
" cv2.rectangle(img_color, (x, y), (x + w, y + h), (0, 255, 0), 2)\n",
" plt.imshow(cv2.cvtColor(img_color, cv2.COLOR_BGR2RGB))\n",
" plt.axis('off')\n",
" plt.show()\n",
"\n",
"# 10. Код для сбора processed_plates\n",
"def gather_processed_plates(image_paths, padding=0.1):\n",
" \"\"\"\n",
" Загружает изображения, обнаруживает номерные знаки и извлекает их.\n",
" \n",
" :param image_paths: Список путей к изображениям.\n",
" :param padding: Процент уменьшения области захвата (например, 0.1 для 10%)\n",
" :return: Список преобразованных изображений номерных знаков.\n",
" \"\"\"\n",
" processed_plates = []\n",
" \n",
" for img_path in image_paths:\n",
" image = cv2.imread(img_path)\n",
" \n",
" if image is None:\n",
" print(f\"Не удалось загрузить изображение по пути: {img_path}\")\n",
" processed_plates.append(None)\n",
" continue\n",
"\n",
" img_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)\n",
" \n",
" ret, thresh = cv2.threshold(img_gray, 100, 200, cv2.THRESH_TOZERO_INV)\n",
" \n",
" contours, hierarchy = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n",
"\n",
" plate_found = False \n",
"\n",
" for cnt in contours:\n",
" x, y, w, h = cv2.boundingRect(cnt)\n",
" area = w * h\n",
" aspectRatio = float(w) / h\n",
"\n",
" if aspectRatio >= 3 and area > 600:\n",
" approx = cv2.approxPolyDP(cnt, 0.05 * cv2.arcLength(cnt, True), True)\n",
" \n",
" if len(approx) <= 4 and x > 15:\n",
" rect = cv2.minAreaRect(cnt)\n",
" box = cv2.boxPoints(rect)\n",
" box = np.intp(box)\n",
"\n",
" plate = get_transformed_plate(image, box, padding=padding) \n",
"\n",
" processed_plates.append(plate)\n",
" plate_found = True\n",
" break \n",
"\n",
" if not plate_found:\n",
" print(f\"Номерной знак не найден на изображении: {img_path}\")\n",
" processed_plates.append(None)\n",
" \n",
" return processed_plates\n",
"\n",
"# 11. Функция для распознавания символов с учётом ожидаемого типа\n",
"def recognize_plate_number(characters, expected_types, model, transform, CLASSES, letter_classes, digit_classes, templates, device=\"cpu\"):\n",
" \"\"\"\n",
" Распознает номерной знак, учитывая ожидаемый тип каждого символа, используя модель и шаблоны.\n",
"\n",
" :param characters: Список изображений символов.\n",
" :param expected_types: Список ожидаемых типов символов ('letter' или 'digit').\n",
" :param model: Обученная модель для распознавания символов.\n",
" :param transform: Преобразования для подготовки изображений перед подачей в модель.\n",
" :param CLASSES: Строка с классами.\n",
" :param letter_classes: Список букв.\n",
" :param digit_classes: Список цифр.\n",
" :param templates: Словарь с шаблонными изображениями.\n",
" :param device: Устройство для вычислений ('cpu' или 'cuda').\n",
" :return: Кортеж из двух строк: распознанный номер моделью и распознанный номер шаблонами.\n",
" \"\"\"\n",
" recognized_number_model = \"\"\n",
" recognized_number_template = \"\"\n",
" for idx, (symbol_img, expected_type) in enumerate(zip(characters, expected_types)):\n",
" # --- Модельное распознавание ---\n",
" # Преобразование изображения символа в формат, подходящий для модели\n",
" pil_img = Image.fromarray(symbol_img)\n",
" input_tensor = transform(pil_img).unsqueeze(0).to(device) # Добавляем размерность батча\n",
"\n",
" with torch.no_grad():\n",
" outputs = model(input_tensor)\n",
" probabilities = torch.softmax(outputs, dim=1).cpu().numpy()[0]\n",
"\n",
" if expected_type == 'letter':\n",
" valid_indices = [i for i, c in enumerate(CLASSES) if c in letter_classes]\n",
" elif expected_type == 'digit':\n",
" valid_indices = [i for i, c in enumerate(CLASSES) if c in digit_classes]\n",
" else:\n",
" valid_indices = list(range(len(CLASSES))) # Если тип не определен, рассматриваем все классы\n",
"\n",
" if not valid_indices:\n",
" recognized_char_model = '?'\n",
" else:\n",
" # Выбираем индекс с максимальной вероятностью среди допустимых\n",
" max_prob_idx = valid_indices[np.argmax(probabilities[valid_indices])]\n",
" recognized_char_model = CLASSES[max_prob_idx]\n",
"\n",
" recognized_number_model += recognized_char_model\n",
"\n",
" # --- Шаблонное сопоставление ---\n",
" recognized_char_template = pattern_match_symbol(symbol_img, templates)\n",
" recognized_number_template += recognized_char_template\n",
"\n",
" return recognized_number_model, recognized_number_template\n",
"\n",
"# 12. Загрузка и подготовка модели\n",
"class SimpleCNN(nn.Module):\n",
" def __init__(self, num_classes):\n",
" super(SimpleCNN, self).__init__()\n",
" self.conv_layers = nn.Sequential(\n",
" nn.Conv2d(1, 32, kernel_size=3, padding=1), # Увеличиваем количество фильтров\n",
" nn.LeakyReLU(negative_slope=0.1), # Используем LeakyReLU вместо ReLU\n",
" nn.BatchNorm2d(32), # Нормализация\n",
" nn.MaxPool2d(kernel_size=2, stride=2),\n",
" \n",
" nn.Conv2d(32, 64, kernel_size=3, padding=1),\n",
" nn.ELU(), # Используем ELU вместо ReLU\n",
" nn.BatchNorm2d(64),\n",
" nn.MaxPool2d(kernel_size=2, stride=2),\n",
" \n",
" nn.Conv2d(64, 128, kernel_size=3, padding=1),\n",
" nn.SiLU(), # Используем SiLU (Swish) вместо ReLU\n",
" nn.BatchNorm2d(128),\n",
" nn.MaxPool2d(kernel_size=2, stride=2)\n",
" )\n",
" self.fc_layers = nn.Sequential(\n",
" nn.Linear(128 * 3 * 3, 256),\n",
" nn.GELU(), # Используем GELU для полносвязного слоя\n",
" nn.Dropout(0.5),\n",
" nn.Linear(256, num_classes)\n",
" )\n",
"\n",
" def forward(self, x):\n",
" x = self.conv_layers(x)\n",
" x = x.view(x.size(0), -1)\n",
" x = self.fc_layers(x)\n",
" return x\n",
" \n",
"# Определение классов и групп\n",
"CLASSES = \"ABEKMHOPCTYX0123456789\"\n",
"NUM_CLASSES = len(CLASSES)\n",
"DEVICE = \"cpu\" \n",
"letter_classes = list(\"ABEKMHOPCTYX\")\n",
"digit_classes = list(\"0123456789\")\n",
"\n",
"# Загрузка модели\n",
"model = SimpleCNN(NUM_CLASSES).to(DEVICE)\n",
"model_path = \"best_model.pth\"\n",
"if not os.path.exists(model_path):\n",
" raise FileNotFoundError(f\"Файл модели '{model_path}' не найден.\")\n",
"model.load_state_dict(torch.load(model_path, map_location=DEVICE)) \n",
"model.eval()\n",
"\n",
"# Определение преобразований\n",
"transform = transforms.Compose([\n",
" transforms.Grayscale(),\n",
" transforms.ToTensor(),\n",
" transforms.Normalize((0.5,), (0.5,))\n",
"])\n",
"\n",
"# 13. Функция для загрузки шаблонов\n",
"def load_templates(template_dir, classes, transform):\n",
" \"\"\"\n",
" Загрузить и предварительно обработать шаблонные изображения из указанной директории.\n",
"\n",
" :param template_dir: Путь к директории с шаблонными изображениями.\n",
" :param classes: Строка с классами.\n",
" :param transform: Преобразования для подготовки изображений.\n",
" :return: Словарь, сопоставляющий класс с изображением шаблона.\n",
" \"\"\"\n",
" templates = {}\n",
" for cls in classes:\n",
" template_path = os.path.join(template_dir, f\"{cls}.png\")\n",
" if not os.path.exists(template_path):\n",
" print(f\"Шаблон для класса '{cls}' не найден по пути: {template_path}\")\n",
" continue\n",
" template_img = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)\n",
" if template_img is None:\n",
" print(f\"Не удалось загрузить шаблонное изображение для класса '{cls}' из {template_path}\")\n",
" continue\n",
" # Предварительная обработка: изменение размера и нормализация\n",
" template_resized = resize_with_padding(template_img, size=28, padding=2, bg_color=255)\n",
" templates[cls] = template_resized\n",
" return templates\n",
"\n",
"# 14. Функция для шаблонного сопоставления\n",
"def pattern_match_symbol(symbol_img, templates):\n",
" \"\"\"\n",
" Выполняет шаблонное сопоставление для распознавания символа.\n",
"\n",
" :param symbol_img: Градация серого изображения символа (numpy array).\n",
" :param templates: Словарь с шаблонными изображениями.\n",
" :return: Распознанный класс символа или '?'.\n",
" \"\"\"\n",
" best_match = '?'\n",
" best_score = -1 # Инициализируем с очень низкого значения\n",
" for cls, template in templates.items():\n",
" # Убедимся, что оба изображения в формате uint8\n",
" symbol = symbol_img.astype(np.uint8)\n",
" template = template.astype(np.uint8)\n",
" # Применяем пороговую обработку, если необходимо\n",
" # Для шаблонного сопоставления оба изображения должны быть однотонными\n",
" # Например, бинаризация\n",
" _, symbol_thresh = cv2.threshold(symbol, 128, 255, cv2.THRESH_BINARY_INV)\n",
" _, template_thresh = cv2.threshold(template, 128, 255, cv2.THRESH_BINARY_INV)\n",
" # Выполняем шаблонное сопоставление\n",
" res = cv2.matchTemplate(symbol_thresh, template_thresh, cv2.TM_CCOEFF_NORMED)\n",
" score = res[0][0]\n",
" if score > best_score:\n",
" best_score = score\n",
" best_match = cls\n",
" # Определяем порог для принятия совпадения\n",
" threshold = 0.3 # Этот параметр можно настроить\n",
" if best_score < threshold:\n",
" return '?'\n",
" return best_match\n",
"\n",
"# 15. Пример использования\n",
"if __name__ == \"__main__\":\n",
" # Пути к изображениям\n",
" images = ['img/1.jpg', 'img/2.jpg', 'img/3.jpg'] # Замените на ваши пути\n",
"\n",
" # Путь к директории с шаблонами\n",
" template_dir = 'templates'\n",
"\n",
" # Загрузка шаблонов\n",
" templates = load_templates(template_dir, CLASSES, transform)\n",
"\n",
" # Сборка processed_plates\n",
" processed_plates = gather_processed_plates(images, padding=0.1)\n",
"\n",
" num_plates = len(processed_plates)\n",
"\n",
" cols = min(num_plates, 3)\n",
" rows = (num_plates + cols - 1) // cols \n",
"\n",
" fig, axs = plt.subplots(rows, cols, figsize=(5 * cols, 5 * rows))\n",
" if num_plates == 1:\n",
" axs = [axs] \n",
" else:\n",
" axs = axs.flatten()\n",
"\n",
" for i, plate in enumerate(processed_plates):\n",
" ax = axs[i]\n",
" if plate is not None:\n",
" if len(plate.shape) == 3:\n",
" plate_rgb = cv2.cvtColor(plate, cv2.COLOR_BGR2RGB)\n",
" ax.imshow(plate_rgb)\n",
" else:\n",
" ax.imshow(plate, cmap='gray')\n",
" else:\n",
" ax.text(0.5, 0.5, 'Номер не найден', horizontalalignment='center', verticalalignment='center', fontsize=12, color='red')\n",
" \n",
" ax.set_title(f'Номер {i+1}')\n",
" ax.axis('off')\n",
" \n",
" # Удаление лишних субплотов, если они есть\n",
" for j in range(i + 1, len(axs)):\n",
" fig.delaxes(axs[j])\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" # Обработка и отображение извлечённых символов\n",
" for plate_index, plate in enumerate(processed_plates):\n",
" # Проверка корректности загрузки изображения\n",
" if plate is None:\n",
" print(f\"Номерной знак {plate_index + 1} не обработан.\")\n",
" continue\n",
"\n",
" # Обработка номерного знака для извлечения символов\n",
" processed_plate, characters, expected_types = process_plate(plate)\n",
"\n",
" # Распознавание номера\n",
" recognized_number_model, recognized_number_template = recognize_plate_number(\n",
" characters, \n",
" expected_types, \n",
" model, \n",
" transform, \n",
" CLASSES, \n",
" letter_classes, \n",
" digit_classes, \n",
" templates, \n",
" device=DEVICE\n",
" )\n",
"\n",
" # Отображение результатов\n",
" plt.figure(figsize=(15, 5))\n",
"\n",
" # Исходный номерной знак\n",
" plt.subplot(1, 3, 1)\n",
" plt.title(\"Исходный Номерной Знак\")\n",
" if len(plate.shape) == 3:\n",
" plt.imshow(cv2.cvtColor(plate, cv2.COLOR_BGR2RGB))\n",
" else:\n",
" plt.imshow(plate, cmap='gray')\n",
" plt.axis('off')\n",
"\n",
" # Обработанное изображение (градации серого и размытие)\n",
" plt.subplot(1, 3, 2)\n",
" plt.title(\"Обработанный Номерной Знак\")\n",
" plt.imshow(processed_plate, cmap='gray')\n",
" plt.axis('off')\n",
"\n",
" # Извлечённые символы\n",
" plt.subplot(1, 3, 3)\n",
" plt.title(\"Извлечённые Символы\")\n",
" if characters:\n",
" # Располагаем символы в горизонтальном ряду\n",
" symbols_grid = np.hstack(characters)\n",
" plt.imshow(symbols_grid, cmap='gray')\n",
" else:\n",
" plt.text(0.5, 0.5, 'Символы не обнаружены', ha='center', va='center', fontsize=12)\n",
" plt.axis('off')\n",
"\n",
" # Добавление заголовка с распознанным номером обоими методами\n",
" plt.suptitle(f\"Результат Обработки Номерного Знака {plate_index + 1}:\\n\"\n",
" f\"Модель: {recognized_number_model}\\n\"\n",
" f\"Шаблоны: {recognized_number_template}\", fontsize=16, color='blue')\n",
"\n",
" plt.show()\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.13"
}
},
"nbformat": 4,
"nbformat_minor": 2
}